From fe5c1968bcdf19cc5f3e339cc6a8927cf6de6532 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Mon, 5 Aug 2024 21:58:03 +0400 Subject: [PATCH 01/22] fix(rnd): Cleanup block names and add explicit block name check (#7709) --- .../autogpt_server/blocks/__init__.py | 15 ++++++++++++++- .../blocks/{if_block.py => branching.py} | 0 .../autogpt_server/blocks/{read_csv.py => csv.py} | 0 .../blocks/{discordblock.py => discord.py} | 0 .../blocks/{foreach_block.py => iteration.py} | 0 .../blocks/{create_medium_post.py => medium.py} | 0 .../blocks/{rss-reader-block.py => rss.py} | 0 .../autogpt_server/blocks/search.py | 6 +++--- .../blocks/{youtube_transcriber.py => youtube.py} | 6 +++--- .../autogpt_server/server/server.py | 1 - 10 files changed, 20 insertions(+), 8 deletions(-) rename rnd/autogpt_server/autogpt_server/blocks/{if_block.py => branching.py} (100%) rename rnd/autogpt_server/autogpt_server/blocks/{read_csv.py => csv.py} (100%) rename rnd/autogpt_server/autogpt_server/blocks/{discordblock.py => discord.py} (100%) rename rnd/autogpt_server/autogpt_server/blocks/{foreach_block.py => iteration.py} (100%) rename rnd/autogpt_server/autogpt_server/blocks/{create_medium_post.py => medium.py} (100%) rename rnd/autogpt_server/autogpt_server/blocks/{rss-reader-block.py => rss.py} (100%) rename rnd/autogpt_server/autogpt_server/blocks/{youtube_transcriber.py => youtube.py} (94%) diff --git a/rnd/autogpt_server/autogpt_server/blocks/__init__.py b/rnd/autogpt_server/autogpt_server/blocks/__init__.py index f177b60bd0..e2ac27bd1a 100644 --- a/rnd/autogpt_server/autogpt_server/blocks/__init__.py +++ b/rnd/autogpt_server/autogpt_server/blocks/__init__.py @@ -1,6 +1,7 @@ import glob import importlib import os +import re from pathlib import Path from autogpt_server.data.block import Block @@ -15,6 +16,11 @@ modules = [ if os.path.isfile(f) and f.endswith(".py") and not f.endswith("__init__.py") ] for module in modules: + if not re.match("^[a-z_]+$", module): + raise ValueError( + f"Block module {module} error: module name must be lowercase, separated by underscores, and contain only alphabet characters" + ) + importlib.import_module(f".{module}", package=__name__) AVAILABLE_MODULES.append(module) @@ -30,9 +36,16 @@ def all_subclasses(clz): for cls in all_subclasses(Block): - if not cls.__name__.endswith("Block"): + name = cls.__name__ + + if cls.__name__.endswith("Base"): continue + if not cls.__name__.endswith("Block"): + raise ValueError( + f"Block class {cls.__name__} does not end with 'Block', If you are creating an abstract class, please name the class with 'Base' at the end" + ) + block = cls() if not isinstance(block.id, str) or len(block.id) != 36: diff --git a/rnd/autogpt_server/autogpt_server/blocks/if_block.py b/rnd/autogpt_server/autogpt_server/blocks/branching.py similarity index 100% rename from rnd/autogpt_server/autogpt_server/blocks/if_block.py rename to rnd/autogpt_server/autogpt_server/blocks/branching.py diff --git a/rnd/autogpt_server/autogpt_server/blocks/read_csv.py b/rnd/autogpt_server/autogpt_server/blocks/csv.py similarity index 100% rename from rnd/autogpt_server/autogpt_server/blocks/read_csv.py rename to rnd/autogpt_server/autogpt_server/blocks/csv.py diff --git a/rnd/autogpt_server/autogpt_server/blocks/discordblock.py b/rnd/autogpt_server/autogpt_server/blocks/discord.py similarity index 100% rename from rnd/autogpt_server/autogpt_server/blocks/discordblock.py rename to rnd/autogpt_server/autogpt_server/blocks/discord.py diff --git a/rnd/autogpt_server/autogpt_server/blocks/foreach_block.py b/rnd/autogpt_server/autogpt_server/blocks/iteration.py similarity index 100% rename from rnd/autogpt_server/autogpt_server/blocks/foreach_block.py rename to rnd/autogpt_server/autogpt_server/blocks/iteration.py diff --git a/rnd/autogpt_server/autogpt_server/blocks/create_medium_post.py b/rnd/autogpt_server/autogpt_server/blocks/medium.py similarity index 100% rename from rnd/autogpt_server/autogpt_server/blocks/create_medium_post.py rename to rnd/autogpt_server/autogpt_server/blocks/medium.py diff --git a/rnd/autogpt_server/autogpt_server/blocks/rss-reader-block.py b/rnd/autogpt_server/autogpt_server/blocks/rss.py similarity index 100% rename from rnd/autogpt_server/autogpt_server/blocks/rss-reader-block.py rename to rnd/autogpt_server/autogpt_server/blocks/rss.py diff --git a/rnd/autogpt_server/autogpt_server/blocks/search.py b/rnd/autogpt_server/autogpt_server/blocks/search.py index 46cd3b4867..21c457323b 100644 --- a/rnd/autogpt_server/autogpt_server/blocks/search.py +++ b/rnd/autogpt_server/autogpt_server/blocks/search.py @@ -131,7 +131,7 @@ class WebScraperBlock(Block, GetRequest): yield "error", f"Request to Jina-ai Reader failed: {e}" -class GetOpenWeatherMapWeather(Block, GetRequest): +class GetOpenWeatherMapBlock(Block, GetRequest): class Input(BlockSchema): location: str api_key: BlockSecret = SecretField(key="openweathermap_api_key") @@ -146,8 +146,8 @@ class GetOpenWeatherMapWeather(Block, GetRequest): def __init__(self): super().__init__( id="f7a8b2c3-6d4e-5f8b-9e7f-6d4e5f8b9e7f", - input_schema=GetOpenWeatherMapWeather.Input, - output_schema=GetOpenWeatherMapWeather.Output, + input_schema=GetOpenWeatherMapBlock.Input, + output_schema=GetOpenWeatherMapBlock.Output, test_input={ "location": "New York", "api_key": "YOUR_API_KEY", diff --git a/rnd/autogpt_server/autogpt_server/blocks/youtube_transcriber.py b/rnd/autogpt_server/autogpt_server/blocks/youtube.py similarity index 94% rename from rnd/autogpt_server/autogpt_server/blocks/youtube_transcriber.py rename to rnd/autogpt_server/autogpt_server/blocks/youtube.py index 3085085514..91248152c0 100644 --- a/rnd/autogpt_server/autogpt_server/blocks/youtube_transcriber.py +++ b/rnd/autogpt_server/autogpt_server/blocks/youtube.py @@ -7,7 +7,7 @@ from autogpt_server.data.block import Block, BlockOutput, BlockSchema from autogpt_server.data.model import SchemaField -class YouTubeTranscriber(Block): +class YouTubeTranscriberBlock(Block): class Input(BlockSchema): youtube_url: str = SchemaField( description="The URL of the YouTube video to transcribe", @@ -24,8 +24,8 @@ class YouTubeTranscriber(Block): def __init__(self): super().__init__( id="f3a8f7e1-4b1d-4e5f-9f2a-7c3d5a2e6b4c", - input_schema=YouTubeTranscriber.Input, - output_schema=YouTubeTranscriber.Output, + input_schema=YouTubeTranscriberBlock.Input, + output_schema=YouTubeTranscriberBlock.Output, test_input={"youtube_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"}, test_output=[ ("video_id", "dQw4w9WgXcQ"), diff --git a/rnd/autogpt_server/autogpt_server/server/server.py b/rnd/autogpt_server/autogpt_server/server/server.py index 2fee638322..bc4d5ecaf5 100644 --- a/rnd/autogpt_server/autogpt_server/server/server.py +++ b/rnd/autogpt_server/autogpt_server/server/server.py @@ -1,5 +1,4 @@ import asyncio -import uuid from collections import defaultdict from contextlib import asynccontextmanager from typing import Annotated, Any, Dict From fbad0d01ee736f4ffbe4803db89f2d2d0b9c0667 Mon Sep 17 00:00:00 2001 From: Aarushi <50577581+aarushik93@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:12:57 +0100 Subject: [PATCH 02/22] feat(rnd, infra): Add deployment for frontend (#7705) * add helm charts * dont check in secrets --- rnd/infra/helm/autogpt_builder/.helmignore | 23 ++++++ rnd/infra/helm/autogpt_builder/Chart.yaml | 24 ++++++ .../helm/autogpt_builder/templates/NOTES.txt | 22 ++++++ .../autogpt_builder/templates/_helpers.tpl | 62 +++++++++++++++ .../autogpt_builder/templates/deployment.yaml | 68 ++++++++++++++++ .../helm/autogpt_builder/templates/hpa.yaml | 32 ++++++++ .../autogpt_builder/templates/ingress.yaml | 61 +++++++++++++++ .../templates/managedcert.yaml | 7 ++ .../autogpt_builder/templates/service.yaml | 15 ++++ .../templates/serviceaccount.yaml | 13 ++++ .../templates/tests/test-connection.yaml | 15 ++++ .../helm/autogpt_builder/values.dev.yaml | 77 +++++++++++++++++++ rnd/infra/helm/autogpt_builder/values.yaml | 76 ++++++++++++++++++ 13 files changed, 495 insertions(+) create mode 100644 rnd/infra/helm/autogpt_builder/.helmignore create mode 100644 rnd/infra/helm/autogpt_builder/Chart.yaml create mode 100644 rnd/infra/helm/autogpt_builder/templates/NOTES.txt create mode 100644 rnd/infra/helm/autogpt_builder/templates/_helpers.tpl create mode 100644 rnd/infra/helm/autogpt_builder/templates/deployment.yaml create mode 100644 rnd/infra/helm/autogpt_builder/templates/hpa.yaml create mode 100644 rnd/infra/helm/autogpt_builder/templates/ingress.yaml create mode 100644 rnd/infra/helm/autogpt_builder/templates/managedcert.yaml create mode 100644 rnd/infra/helm/autogpt_builder/templates/service.yaml create mode 100644 rnd/infra/helm/autogpt_builder/templates/serviceaccount.yaml create mode 100644 rnd/infra/helm/autogpt_builder/templates/tests/test-connection.yaml create mode 100644 rnd/infra/helm/autogpt_builder/values.dev.yaml create mode 100644 rnd/infra/helm/autogpt_builder/values.yaml diff --git a/rnd/infra/helm/autogpt_builder/.helmignore b/rnd/infra/helm/autogpt_builder/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/rnd/infra/helm/autogpt_builder/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/rnd/infra/helm/autogpt_builder/Chart.yaml b/rnd/infra/helm/autogpt_builder/Chart.yaml new file mode 100644 index 0000000000..96653b89ea --- /dev/null +++ b/rnd/infra/helm/autogpt_builder/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: autogpt_builder +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/rnd/infra/helm/autogpt_builder/templates/NOTES.txt b/rnd/infra/helm/autogpt_builder/templates/NOTES.txt new file mode 100644 index 0000000000..8ce6c44ba8 --- /dev/null +++ b/rnd/infra/helm/autogpt_builder/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "autogpt_builder.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "autogpt_builder.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "autogpt_builder.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "autogpt_builder.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/rnd/infra/helm/autogpt_builder/templates/_helpers.tpl b/rnd/infra/helm/autogpt_builder/templates/_helpers.tpl new file mode 100644 index 0000000000..8e65eaf4be --- /dev/null +++ b/rnd/infra/helm/autogpt_builder/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "autogpt_builder.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "autogpt_builder.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "autogpt_builder.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "autogpt_builder.labels" -}} +helm.sh/chart: {{ include "autogpt_builder.chart" . }} +{{ include "autogpt_builder.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "autogpt_builder.selectorLabels" -}} +app.kubernetes.io/name: {{ include "autogpt_builder.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "autogpt_builder.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "autogpt_builder.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/rnd/infra/helm/autogpt_builder/templates/deployment.yaml b/rnd/infra/helm/autogpt_builder/templates/deployment.yaml new file mode 100644 index 0000000000..1c19a77ac3 --- /dev/null +++ b/rnd/infra/helm/autogpt_builder/templates/deployment.yaml @@ -0,0 +1,68 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "autogpt_builder.fullname" . }} + labels: + {{- include "autogpt_builder.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "autogpt_builder.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "autogpt_builder.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "autogpt_builder.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/rnd/infra/helm/autogpt_builder/templates/hpa.yaml b/rnd/infra/helm/autogpt_builder/templates/hpa.yaml new file mode 100644 index 0000000000..f3e5b90e83 --- /dev/null +++ b/rnd/infra/helm/autogpt_builder/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "autogpt_builder.fullname" . }} + labels: + {{- include "autogpt_builder.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "autogpt_builder.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/rnd/infra/helm/autogpt_builder/templates/ingress.yaml b/rnd/infra/helm/autogpt_builder/templates/ingress.yaml new file mode 100644 index 0000000000..2e0c5f75f7 --- /dev/null +++ b/rnd/infra/helm/autogpt_builder/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "autogpt_builder.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "autogpt_builder.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/rnd/infra/helm/autogpt_builder/templates/managedcert.yaml b/rnd/infra/helm/autogpt_builder/templates/managedcert.yaml new file mode 100644 index 0000000000..c1d9372478 --- /dev/null +++ b/rnd/infra/helm/autogpt_builder/templates/managedcert.yaml @@ -0,0 +1,7 @@ +apiVersion: networking.gke.io/v1 +kind: ManagedCertificate +metadata: + name: {{ include "autogpt-builder.fullname" . }}-cert +spec: + domains: + - {{ .Values.domain }} \ No newline at end of file diff --git a/rnd/infra/helm/autogpt_builder/templates/service.yaml b/rnd/infra/helm/autogpt_builder/templates/service.yaml new file mode 100644 index 0000000000..050b932111 --- /dev/null +++ b/rnd/infra/helm/autogpt_builder/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "autogpt_builder.fullname" . }} + labels: + {{- include "autogpt_builder.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "autogpt_builder.selectorLabels" . | nindent 4 }} diff --git a/rnd/infra/helm/autogpt_builder/templates/serviceaccount.yaml b/rnd/infra/helm/autogpt_builder/templates/serviceaccount.yaml new file mode 100644 index 0000000000..a50ea11db8 --- /dev/null +++ b/rnd/infra/helm/autogpt_builder/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "autogpt_builder.serviceAccountName" . }} + labels: + {{- include "autogpt_builder.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/rnd/infra/helm/autogpt_builder/templates/tests/test-connection.yaml b/rnd/infra/helm/autogpt_builder/templates/tests/test-connection.yaml new file mode 100644 index 0000000000..3ca1ba1119 --- /dev/null +++ b/rnd/infra/helm/autogpt_builder/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "autogpt_builder.fullname" . }}-test-connection" + labels: + {{- include "autogpt_builder.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "autogpt_builder.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/rnd/infra/helm/autogpt_builder/values.dev.yaml b/rnd/infra/helm/autogpt_builder/values.dev.yaml new file mode 100644 index 0000000000..d618aa958e --- /dev/null +++ b/rnd/infra/helm/autogpt_builder/values.dev.yaml @@ -0,0 +1,77 @@ +# dev values, overwrite base values as needed. + +image: + repository: us-east1-docker.pkg.dev/agpt-dev/agpt-builder-dev/agpt-builder-dev + pullPolicy: Always + tag: "latest" + +serviceAccount: + annotations: + iam.gke.io/gcp-service-account: "dev-agpt-builder-sa@agpt-dev.iam.gserviceaccount.com" + name: "dev-agpt-builder-sa" + +service: + type: ClusterIP + port: 8000 + targetPort: 3000 + annotations: + cloud.google.com/neg: '{"ingress": true}' + +ingress: + enabled: true + className: "gce" + annotations: + kubernetes.io/ingress.class: gce + kubernetes.io/ingress.global-static-ip-name: "agpt-dev-agpt-builder-ip" + networking.gke.io/managed-certificates: "autogpt-builder-cert" + kubernetes.io/ingress.allow-http: "true" + hosts: + - host: dev-builder.agpt.co + paths: + - path: / + pathType: Prefix + backend: + service: + name: autogpt-builder + port: 8000 + defaultBackend: + service: + name: autogpt-builder + port: + number: 8000 + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + +livenessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 +readinessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + +domain: "dev-builder.agpt.co" + + +env: + APP_ENV: "dev" + NEXT_PUBLIC_AGPT_SERVER_URL: "http://agpt-server:8000/api" + GOOGLE_CLIENT_ID: "638488734936-ka0bvq73ub3h4cb6013s3lftsl5l04nu.apps.googleusercontent.com" + GOOGLE_CLIENT_SECRET: "" + NEXT_PUBLIC_SUPABASE_URL: "https://adfjtextkuilwuhzdjpf.supabase.co" + NEXT_PUBLIC_SUPABASE_ANON_KEY: "" \ No newline at end of file diff --git a/rnd/infra/helm/autogpt_builder/values.yaml b/rnd/infra/helm/autogpt_builder/values.yaml new file mode 100644 index 0000000000..3b4b64e6e6 --- /dev/null +++ b/rnd/infra/helm/autogpt_builder/values.yaml @@ -0,0 +1,76 @@ +# base values, environment specific variables should be specified/overwritten in environment values + +replicaCount: 1 + +image: + repository: us-east1-docker.pkg.dev/agpt-dev/agpt-builder-dev/agpt-builder-dev + pullPolicy: IfNotPresent + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + automount: true + annotations: {} + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + +securityContext: {} + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetMemoryUtilizationPercentage: 80 + +volumes: [] + +volumeMounts: [] + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +domain: "" From e6cc8687a5e2b70b9afc228243dbc05da05dfbea Mon Sep 17 00:00:00 2001 From: Andy Hooker <58448663+andrewhooker2@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:13:08 -0500 Subject: [PATCH 03/22] feat(builder): Refactor components and types for monitoring page streamlining (#7714) --- rnd/autogpt_builder/src/app/monitor/page.tsx | 739 +----------------- .../src/components/monitor/AgentFlowList.tsx | 187 +++++ .../src/components/monitor/FlowInfo.tsx | 134 ++++ .../src/components/monitor/FlowRunInfo.tsx | 66 ++ .../components/monitor/FlowRunStatusBadge.tsx | 25 + .../src/components/monitor/FlowRunsList.tsx | 68 ++ .../src/components/monitor/FlowRunsStatus.tsx | 114 +++ .../components/monitor/FlowRunsTimeline.tsx | 170 ++++ .../src/components/monitor/index.ts | 6 + rnd/autogpt_builder/src/lib/types.ts | 13 + 10 files changed, 793 insertions(+), 729 deletions(-) create mode 100644 rnd/autogpt_builder/src/components/monitor/AgentFlowList.tsx create mode 100644 rnd/autogpt_builder/src/components/monitor/FlowInfo.tsx create mode 100644 rnd/autogpt_builder/src/components/monitor/FlowRunInfo.tsx create mode 100644 rnd/autogpt_builder/src/components/monitor/FlowRunStatusBadge.tsx create mode 100644 rnd/autogpt_builder/src/components/monitor/FlowRunsList.tsx create mode 100644 rnd/autogpt_builder/src/components/monitor/FlowRunsStatus.tsx create mode 100644 rnd/autogpt_builder/src/components/monitor/FlowRunsTimeline.tsx create mode 100644 rnd/autogpt_builder/src/components/monitor/index.ts create mode 100644 rnd/autogpt_builder/src/lib/types.ts diff --git a/rnd/autogpt_builder/src/app/monitor/page.tsx b/rnd/autogpt_builder/src/app/monitor/page.tsx index 2a7f7e9983..c88b386094 100644 --- a/rnd/autogpt_builder/src/app/monitor/page.tsx +++ b/rnd/autogpt_builder/src/app/monitor/page.tsx @@ -1,66 +1,20 @@ "use client"; import React, { useEffect, useState } from "react"; -import Link from "next/link"; -import moment from "moment"; -import { - ComposedChart, - DefaultLegendContentProps, - Legend, - Line, - ResponsiveContainer, - Scatter, - Tooltip, - XAxis, - YAxis, -} from "recharts"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; + import AutoGPTServerAPI, { - Graph, GraphMeta, NodeExecutionResult, - safeCopyGraph, } from "@/lib/autogpt-server-api"; + +import { Card } from "@/components/ui/card"; +import { FlowRun } from "@/lib/types"; import { - ChevronDownIcon, - ClockIcon, - EnterIcon, - ExitIcon, - Pencil2Icon, -} from "@radix-ui/react-icons"; -import { cn, exportAsJSONFile, hashString } from "@/lib/utils"; -import { Badge } from "@/components/ui/badge"; -import { Button, buttonVariants } from "@/components/ui/button"; -import { Calendar } from "@/components/ui/calendar"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { AgentImportForm } from "@/components/agent-import-form"; + AgentFlowList, + FlowInfo, + FlowRunInfo, + FlowRunsList, + FlowRunsStats, +} from "@/components/monitor"; const Monitor = () => { const [flows, setFlows] = useState([]); @@ -165,19 +119,6 @@ const Monitor = () => { ); }; -type FlowRun = { - id: string; - graphID: string; - graphVersion: number; - status: "running" | "waiting" | "success" | "failed"; - startTime: number; // unix timestamp (ms) - endTime: number; // unix timestamp (ms) - duration: number; // seconds - totalRunTime: number; // seconds - - nodeExecutionResults: NodeExecutionResult[]; -}; - function flowRunFromNodeExecutionResults( nodeExecutionResults: NodeExecutionResult[], ): FlowRun { @@ -230,664 +171,4 @@ function flowRunFromNodeExecutionResults( }; } -const AgentFlowList = ({ - flows, - flowRuns, - selectedFlow, - onSelectFlow, - className, -}: { - flows: GraphMeta[]; - flowRuns?: FlowRun[]; - selectedFlow: GraphMeta | null; - onSelectFlow: (f: GraphMeta) => void; - className?: string; -}) => { - const [templates, setTemplates] = useState([]); - const api = new AutoGPTServerAPI(); - useEffect(() => { - api.listTemplates().then((templates) => setTemplates(templates)); - }, []); - - return ( - - - Agents - -
- {/* Split "Create" button */} - - - {/* https://ui.shadcn.com/docs/components/dialog#notes */} - - - - - - - - - Import from file - - - {templates.length > 0 && ( - <> - {/* List of templates */} - - Use a template - {templates.map((template) => ( - { - api - .createGraph(template.id, template.version) - .then((newGraph) => { - window.location.href = `/build?flowID=${newGraph.id}`; - }); - }} - > - {template.name} - - ))} - - )} - - - - - - Import an Agent (template) from a file - - - - -
-
- - - - - - Name - {/* Status */} - {/* Last updated */} - {flowRuns && ( - - # of runs - - )} - {flowRuns && Last run} - - - - {flows - .map((flow) => { - let runCount = 0, - lastRun: FlowRun | null = null; - if (flowRuns) { - const _flowRuns = flowRuns.filter( - (r) => r.graphID == flow.id, - ); - runCount = _flowRuns.length; - lastRun = - runCount == 0 - ? null - : _flowRuns.reduce((a, c) => - a.startTime > c.startTime ? a : c, - ); - } - return { flow, runCount, lastRun }; - }) - .sort((a, b) => { - if (!a.lastRun && !b.lastRun) return 0; - if (!a.lastRun) return 1; - if (!b.lastRun) return -1; - return b.lastRun.startTime - a.lastRun.startTime; - }) - .map(({ flow, runCount, lastRun }) => ( - onSelectFlow(flow)} - data-state={selectedFlow?.id == flow.id ? "selected" : null} - > - {flow.name} - {/* */} - {/* - {flow.updatedAt ?? "???"} - */} - {flowRuns && ( - - {runCount} - - )} - {flowRuns && - (!lastRun ? ( - - ) : ( - - {moment(lastRun.startTime).fromNow()} - - ))} - - ))} - -
-
-
- ); -}; - -const FlowStatusBadge = ({ - status, -}: { - status: "active" | "disabled" | "failing"; -}) => ( - - {status} - -); - -const FlowRunsList: React.FC<{ - flows: GraphMeta[]; - runs: FlowRun[]; - className?: string; - selectedRun?: FlowRun | null; - onSelectRun: (r: FlowRun) => void; -}> = ({ flows, runs, selectedRun, onSelectRun, className }) => ( - - - Runs - - - - - - Agent - Started - Status - Duration - - - - {runs.map((run) => ( - onSelectRun(run)} - data-state={selectedRun?.id == run.id ? "selected" : null} - > - - {flows.find((f) => f.id == run.graphID)!.name} - - {moment(run.startTime).format("HH:mm")} - - - - {formatDuration(run.duration)} - - ))} - -
-
-
-); - -const FlowRunStatusBadge: React.FC<{ - status: FlowRun["status"]; - className?: string; -}> = ({ status, className }) => ( - - {status} - -); - -const FlowInfo: React.FC< - React.HTMLAttributes & { - flow: GraphMeta; - flowRuns: FlowRun[]; - flowVersion?: number | "all"; - } -> = ({ flow, flowRuns, flowVersion, ...props }) => { - const api = new AutoGPTServerAPI(); - - const [flowVersions, setFlowVersions] = useState(null); - const [selectedVersion, setSelectedFlowVersion] = useState( - flowVersion ?? "all", - ); - const selectedFlowVersion: Graph | undefined = flowVersions?.find( - (v) => - v.version == (selectedVersion == "all" ? flow.version : selectedVersion), - ); - - useEffect(() => { - api.getGraphAllVersions(flow.id).then((result) => setFlowVersions(result)); - }, [flow.id]); - - return ( - - -
- - {flow.name} v{flow.version} - -

- Agent ID: {flow.id} -

-
-
- {(flowVersions?.length ?? 0) > 1 && ( - - - - - - Choose a version - - - setSelectedFlowVersion( - choice == "all" ? choice : Number(choice), - ) - } - > - - All versions - - {flowVersions?.map((v) => ( - - Version {v.version} - {v.is_active ? " (active)" : ""} - - ))} - - - - )} - - Edit - - -
-
- - - r.graphID == flow.id && - (selectedVersion == "all" || r.graphVersion == selectedVersion), - )} - /> - -
- ); -}; - -const FlowRunInfo: React.FC< - React.HTMLAttributes & { - flow: GraphMeta; - flowRun: FlowRun; - } -> = ({ flow, flowRun, ...props }) => { - if (flowRun.graphID != flow.id) { - throw new Error( - `FlowRunInfo can't be used with non-matching flowRun.flowID and flow.id`, - ); - } - - return ( - - -
- - {flow.name} v{flow.version} - -

- Agent ID: {flow.id} -

-

- Run ID: {flowRun.id} -

-
- - Edit Agent - -
- -

- Status:{" "} - -

-

- Started:{" "} - {moment(flowRun.startTime).format("YYYY-MM-DD HH:mm:ss")} -

-

- Finished:{" "} - {moment(flowRun.endTime).format("YYYY-MM-DD HH:mm:ss")} -

-

- Duration (run time): {flowRun.duration} ( - {flowRun.totalRunTime}) seconds -

- {/*

Total cost: €1,23

*/} -
-
- ); -}; - -const FlowRunsStats: React.FC<{ - flows: GraphMeta[]; - flowRuns: FlowRun[]; - title?: string; - className?: string; -}> = ({ flows, flowRuns, title, className }) => { - /* "dateMin": since the first flow in the dataset - * number > 0: custom date (unix timestamp) - * number < 0: offset relative to Date.now() (in seconds) */ - const [statsSince, setStatsSince] = useState(-24 * 3600); - const statsSinceTimestamp = // unix timestamp or null - typeof statsSince == "string" - ? null - : statsSince < 0 - ? Date.now() + statsSince * 1000 - : statsSince; - const filteredFlowRuns = - statsSinceTimestamp != null - ? flowRuns.filter((fr) => fr.startTime > statsSinceTimestamp) - : flowRuns; - - return ( -
-
- {title || "Stats"} -
- - - - - - - - - - - setStatsSince(selectedDay.getTime()) - } - initialFocus - /> - - - -
-
- -
-
-

- Total runs: {filteredFlowRuns.length} -

-

- Total run time:{" "} - {filteredFlowRuns.reduce((total, run) => total + run.totalRunTime, 0)}{" "} - seconds -

- {/*

Total cost: €1,23

*/} -
-
- ); -}; - -const FlowRunsTimeline = ({ - flows, - flowRuns, - dataMin, - className, -}: { - flows: GraphMeta[]; - flowRuns: FlowRun[]; - dataMin: "dataMin" | number; - className?: string; -}) => ( - /* TODO: make logarithmic? */ - - - { - const now = moment(); - const time = moment(unixTime); - return now.diff(time, "hours") < 24 - ? time.format("HH:mm") - : time.format("YYYY-MM-DD HH:mm"); - }} - name="Time" - scale="time" - /> - (s > 90 ? `${Math.round(s / 60)}m` : `${s}s`)} - /> - { - if (payload && payload.length) { - const data: FlowRun & { time: number; _duration: number } = - payload[0].payload; - const flow = flows.find((f) => f.id === data.graphID); - return ( - -

- Agent: {flow ? flow.name : "Unknown"} -

-

- Status:  - -

-

- Started:{" "} - {moment(data.startTime).format("YYYY-MM-DD HH:mm:ss")} -

-

- Duration / run time:{" "} - {formatDuration(data.duration)} /{" "} - {formatDuration(data.totalRunTime)} -

-
- ); - } - return null; - }} - /> - {flows.map((flow) => ( - fr.graphID == flow.id) - .map((fr) => ({ - ...fr, - time: fr.startTime + fr.totalRunTime * 1000, - _duration: fr.totalRunTime, - }))} - name={flow.name} - fill={`hsl(${(hashString(flow.id) * 137.5) % 360}, 70%, 50%)`} - /> - ))} - {flowRuns.map((run) => ( - - ))} - } - wrapperStyle={{ - bottom: 0, - left: 0, - right: 0, - width: "100%", - display: "flex", - justifyContent: "center", - }} - /> -
-
-); - -const ScrollableLegend: React.FC< - DefaultLegendContentProps & { className?: string } -> = ({ payload, className }) => { - return ( -
- {payload.map((entry, index) => { - if (entry.type == "none") return; - return ( - - - {entry.value} - - ); - })} -
- ); -}; - -function formatDuration(seconds: number): string { - return ( - (seconds < 100 ? seconds.toPrecision(2) : Math.round(seconds)).toString() + - "s" - ); -} - export default Monitor; diff --git a/rnd/autogpt_builder/src/components/monitor/AgentFlowList.tsx b/rnd/autogpt_builder/src/components/monitor/AgentFlowList.tsx new file mode 100644 index 0000000000..0acf6ad5e3 --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/AgentFlowList.tsx @@ -0,0 +1,187 @@ +import AutoGPTServerAPI, { GraphMeta } from "@/lib/autogpt-server-api"; +import React, { useEffect, useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { ChevronDownIcon, EnterIcon } from "@radix-ui/react-icons"; +import { AgentImportForm } from "@/components/agent-import-form"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import moment from "moment/moment"; +import { FlowRun } from "@/lib/types"; + +export const AgentFlowList = ({ + flows, + flowRuns, + selectedFlow, + onSelectFlow, + className, +}: { + flows: GraphMeta[]; + flowRuns?: FlowRun[]; + selectedFlow: GraphMeta | null; + onSelectFlow: (f: GraphMeta) => void; + className?: string; +}) => { + const [templates, setTemplates] = useState([]); + const api = new AutoGPTServerAPI(); + useEffect(() => { + api.listTemplates().then((templates) => setTemplates(templates)); + }, []); + + return ( + + + Agents + +
+ {/* Split "Create" button */} + + + {/* https://ui.shadcn.com/docs/components/dialog#notes */} + + + + + + + + + Import from file + + + {templates.length > 0 && ( + <> + {/* List of templates */} + + Use a template + {templates.map((template) => ( + { + api + .createGraph(template.id, template.version) + .then((newGraph) => { + window.location.href = `/build?flowID=${newGraph.id}`; + }); + }} + > + {template.name} + + ))} + + )} + + + + + + Import an Agent (template) from a file + + + + +
+
+ + + + + + Name + {/* Status */} + {/* Last updated */} + {flowRuns && ( + + # of runs + + )} + {flowRuns && Last run} + + + + {flows + .map((flow) => { + let runCount = 0, + lastRun: FlowRun | null = null; + if (flowRuns) { + const _flowRuns = flowRuns.filter( + (r) => r.graphID == flow.id, + ); + runCount = _flowRuns.length; + lastRun = + runCount == 0 + ? null + : _flowRuns.reduce((a, c) => + a.startTime > c.startTime ? a : c, + ); + } + return { flow, runCount, lastRun }; + }) + .sort((a, b) => { + if (!a.lastRun && !b.lastRun) return 0; + if (!a.lastRun) return 1; + if (!b.lastRun) return -1; + return b.lastRun.startTime - a.lastRun.startTime; + }) + .map(({ flow, runCount, lastRun }) => ( + onSelectFlow(flow)} + data-state={selectedFlow?.id == flow.id ? "selected" : null} + > + {flow.name} + {/* */} + {/* + {flow.updatedAt ?? "???"} + */} + {flowRuns && ( + + {runCount} + + )} + {flowRuns && + (!lastRun ? ( + + ) : ( + + {moment(lastRun.startTime).fromNow()} + + ))} + + ))} + +
+
+
+ ); +}; +export default AgentFlowList; diff --git a/rnd/autogpt_builder/src/components/monitor/FlowInfo.tsx b/rnd/autogpt_builder/src/components/monitor/FlowInfo.tsx new file mode 100644 index 0000000000..0ec6d2187b --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/FlowInfo.tsx @@ -0,0 +1,134 @@ +import React, { useEffect, useState } from "react"; +import AutoGPTServerAPI, { + Graph, + GraphMeta, + safeCopyGraph, +} from "@/lib/autogpt-server-api"; +import { FlowRun } from "@/lib/types"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { ClockIcon, ExitIcon, Pencil2Icon } from "@radix-ui/react-icons"; +import Link from "next/link"; +import { exportAsJSONFile } from "@/lib/utils"; +import { FlowRunsStats } from "@/components/monitor/index"; + +export const FlowInfo: React.FC< + React.HTMLAttributes & { + flow: GraphMeta; + flowRuns: FlowRun[]; + flowVersion?: number | "all"; + } +> = ({ flow, flowRuns, flowVersion, ...props }) => { + const api = new AutoGPTServerAPI(); + + const [flowVersions, setFlowVersions] = useState(null); + const [selectedVersion, setSelectedFlowVersion] = useState( + flowVersion ?? "all", + ); + const selectedFlowVersion: Graph | undefined = flowVersions?.find( + (v) => + v.version == (selectedVersion == "all" ? flow.version : selectedVersion), + ); + + useEffect(() => { + api.getGraphAllVersions(flow.id).then((result) => setFlowVersions(result)); + }, [flow.id]); + + return ( + + +
+ + {flow.name} v{flow.version} + +

+ Agent ID: {flow.id} +

+
+
+ {(flowVersions?.length ?? 0) > 1 && ( + + + + + + Choose a version + + + setSelectedFlowVersion( + choice == "all" ? choice : Number(choice), + ) + } + > + + All versions + + {flowVersions?.map((v) => ( + + Version {v.version} + {v.is_active ? " (active)" : ""} + + ))} + + + + )} + + Edit + + +
+
+ + + r.graphID == flow.id && + (selectedVersion == "all" || r.graphVersion == selectedVersion), + )} + /> + +
+ ); +}; +export default FlowInfo; diff --git a/rnd/autogpt_builder/src/components/monitor/FlowRunInfo.tsx b/rnd/autogpt_builder/src/components/monitor/FlowRunInfo.tsx new file mode 100644 index 0000000000..1ace5207ce --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/FlowRunInfo.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import { GraphMeta } from "@/lib/autogpt-server-api"; +import { FlowRun } from "@/lib/types"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import Link from "next/link"; +import { buttonVariants } from "@/components/ui/button"; +import { Pencil2Icon } from "@radix-ui/react-icons"; +import moment from "moment/moment"; +import { FlowRunStatusBadge } from "@/components/monitor/FlowRunStatusBadge"; + +export const FlowRunInfo: React.FC< + React.HTMLAttributes & { + flow: GraphMeta; + flowRun: FlowRun; + } +> = ({ flow, flowRun, ...props }) => { + if (flowRun.graphID != flow.id) { + throw new Error( + `FlowRunInfo can't be used with non-matching flowRun.flowID and flow.id`, + ); + } + + return ( + + +
+ + {flow.name} v{flow.version} + +

+ Agent ID: {flow.id} +

+

+ Run ID: {flowRun.id} +

+
+ + Edit Agent + +
+ +

+ Status:{" "} + +

+

+ Started:{" "} + {moment(flowRun.startTime).format("YYYY-MM-DD HH:mm:ss")} +

+

+ Finished:{" "} + {moment(flowRun.endTime).format("YYYY-MM-DD HH:mm:ss")} +

+

+ Duration (run time): {flowRun.duration} ( + {flowRun.totalRunTime}) seconds +

+ {/*

Total cost: €1,23

*/} +
+
+ ); +}; +export default FlowRunInfo; diff --git a/rnd/autogpt_builder/src/components/monitor/FlowRunStatusBadge.tsx b/rnd/autogpt_builder/src/components/monitor/FlowRunStatusBadge.tsx new file mode 100644 index 0000000000..f054782223 --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/FlowRunStatusBadge.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { FlowRun } from "@/lib/types"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; + +export const FlowRunStatusBadge: React.FC<{ + status: FlowRun["status"]; + className?: string; +}> = ({ status, className }) => ( + + {status} + +); diff --git a/rnd/autogpt_builder/src/components/monitor/FlowRunsList.tsx b/rnd/autogpt_builder/src/components/monitor/FlowRunsList.tsx new file mode 100644 index 0000000000..ed29355560 --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/FlowRunsList.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { GraphMeta } from "@/lib/autogpt-server-api"; +import { FlowRun } from "@/lib/types"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import moment from "moment/moment"; +import { FlowRunStatusBadge } from "@/components/monitor/FlowRunStatusBadge"; + +export const FlowRunsList: React.FC<{ + flows: GraphMeta[]; + runs: FlowRun[]; + className?: string; + selectedRun?: FlowRun | null; + onSelectRun: (r: FlowRun) => void; +}> = ({ flows, runs, selectedRun, onSelectRun, className }) => ( + + + Runs + + + + + + Agent + Started + Status + Duration + + + + {runs.map((run) => ( + onSelectRun(run)} + data-state={selectedRun?.id == run.id ? "selected" : null} + > + + {flows.find((f) => f.id == run.graphID)!.name} + + {moment(run.startTime).format("HH:mm")} + + + + {formatDuration(run.duration)} + + ))} + +
+
+
+); + +function formatDuration(seconds: number): string { + return ( + (seconds < 100 ? seconds.toPrecision(2) : Math.round(seconds)).toString() + + "s" + ); +} + +export default FlowRunsList; diff --git a/rnd/autogpt_builder/src/components/monitor/FlowRunsStatus.tsx b/rnd/autogpt_builder/src/components/monitor/FlowRunsStatus.tsx new file mode 100644 index 0000000000..cf46134942 --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/FlowRunsStatus.tsx @@ -0,0 +1,114 @@ +import React, { useState } from "react"; +import { GraphMeta } from "@/lib/autogpt-server-api"; +import { FlowRun } from "@/lib/types"; +import { CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Calendar } from "@/components/ui/calendar"; +import { FlowRunsTimeline } from "@/components/monitor/FlowRunsTimeline"; + +export const FlowRunsStatus: React.FC<{ + flows: GraphMeta[]; + flowRuns: FlowRun[]; + title?: string; + className?: string; +}> = ({ flows, flowRuns, title, className }) => { + /* "dateMin": since the first flow in the dataset + * number > 0: custom date (unix timestamp) + * number < 0: offset relative to Date.now() (in seconds) */ + const [statsSince, setStatsSince] = useState(-24 * 3600); + const statsSinceTimestamp = // unix timestamp or null + typeof statsSince == "string" + ? null + : statsSince < 0 + ? Date.now() + statsSince * 1000 + : statsSince; + const filteredFlowRuns = + statsSinceTimestamp != null + ? flowRuns.filter((fr) => fr.startTime > statsSinceTimestamp) + : flowRuns; + + return ( +
+
+ {title || "Stats"} +
+ + + + + + + + + + + setStatsSince(selectedDay.getTime()) + } + initialFocus + /> + + + +
+
+ +
+
+

+ Total runs: {filteredFlowRuns.length} +

+

+ Total run time:{" "} + {filteredFlowRuns.reduce((total, run) => total + run.totalRunTime, 0)}{" "} + seconds +

+ {/*

Total cost: €1,23

*/} +
+
+ ); +}; +export default FlowRunsStatus; diff --git a/rnd/autogpt_builder/src/components/monitor/FlowRunsTimeline.tsx b/rnd/autogpt_builder/src/components/monitor/FlowRunsTimeline.tsx new file mode 100644 index 0000000000..c31efee180 --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/FlowRunsTimeline.tsx @@ -0,0 +1,170 @@ +import { GraphMeta } from "@/lib/autogpt-server-api"; +import { + ComposedChart, + DefaultLegendContentProps, + Legend, + Line, + ResponsiveContainer, + Scatter, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import moment from "moment/moment"; +import { Card } from "@/components/ui/card"; +import { cn, hashString } from "@/lib/utils"; +import React from "react"; +import { FlowRun } from "@/lib/types"; +import { FlowRunStatusBadge } from "@/components/monitor/FlowRunStatusBadge"; + +export const FlowRunsTimeline = ({ + flows, + flowRuns, + dataMin, + className, +}: { + flows: GraphMeta[]; + flowRuns: FlowRun[]; + dataMin: "dataMin" | number; + className?: string; +}) => ( + /* TODO: make logarithmic? */ + + + { + const now = moment(); + const time = moment(unixTime); + return now.diff(time, "hours") < 24 + ? time.format("HH:mm") + : time.format("YYYY-MM-DD HH:mm"); + }} + name="Time" + scale="time" + /> + (s > 90 ? `${Math.round(s / 60)}m` : `${s}s`)} + /> + { + if (payload && payload.length) { + const data: FlowRun & { time: number; _duration: number } = + payload[0].payload; + const flow = flows.find((f) => f.id === data.graphID); + return ( + +

+ Agent: {flow ? flow.name : "Unknown"} +

+

+ Status:  + +

+

+ Started:{" "} + {moment(data.startTime).format("YYYY-MM-DD HH:mm:ss")} +

+

+ Duration / run time:{" "} + {formatDuration(data.duration)} /{" "} + {formatDuration(data.totalRunTime)} +

+
+ ); + } + return null; + }} + /> + {flows.map((flow) => ( + fr.graphID == flow.id) + .map((fr) => ({ + ...fr, + time: fr.startTime + fr.totalRunTime * 1000, + _duration: fr.totalRunTime, + }))} + name={flow.name} + fill={`hsl(${(hashString(flow.id) * 137.5) % 360}, 70%, 50%)`} + /> + ))} + {flowRuns.map((run) => ( + + ))} + } + wrapperStyle={{ + bottom: 0, + left: 0, + right: 0, + width: "100%", + display: "flex", + justifyContent: "center", + }} + /> +
+
+); + +const ScrollableLegend: React.FC< + DefaultLegendContentProps & { className?: string } +> = ({ payload, className }) => { + return ( +
+ {payload?.map((entry, index) => { + if (entry.type == "none") return; + return ( + + + {entry.value} + + ); + })} +
+ ); +}; + +function formatDuration(seconds: number): string { + return ( + (seconds < 100 ? seconds.toPrecision(2) : Math.round(seconds)).toString() + + "s" + ); +} diff --git a/rnd/autogpt_builder/src/components/monitor/index.ts b/rnd/autogpt_builder/src/components/monitor/index.ts new file mode 100644 index 0000000000..0f8f80287c --- /dev/null +++ b/rnd/autogpt_builder/src/components/monitor/index.ts @@ -0,0 +1,6 @@ +export { default as AgentFlowList } from "./AgentFlowList"; +export { default as FlowRunsList } from "./FlowRunsList"; +export { default as FlowInfo } from "./FlowInfo"; +export { default as FlowRunInfo } from "./FlowRunInfo"; +export { default as FlowRunsStats } from "./FlowRunsStatus"; +export { default as FlowRunsTimeline } from "./FlowRunsTimeline"; diff --git a/rnd/autogpt_builder/src/lib/types.ts b/rnd/autogpt_builder/src/lib/types.ts new file mode 100644 index 0000000000..04750a5973 --- /dev/null +++ b/rnd/autogpt_builder/src/lib/types.ts @@ -0,0 +1,13 @@ +import { NodeExecutionResult } from "@/lib/autogpt-server-api"; + +export type FlowRun = { + id: string; + graphID: string; + graphVersion: number; + status: "running" | "waiting" | "success" | "failed"; + startTime: number; // unix timestamp (ms) + endTime: number; // unix timestamp (ms) + duration: number; // seconds + totalRunTime: number; // seconds + nodeExecutionResults: NodeExecutionResult[]; +}; From d82e57719677e972cadf1fc890908dc904db332c Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Tue, 6 Aug 2024 20:32:49 +0200 Subject: [PATCH 04/22] feat(builder): Rewrite & split up node inputs (#7722) - feat(builder): Rewrite & split up `NodeInputField` - Create `NodeObjectInputTree` - Create `NodeGenericInputField` - Create `NodeKeyValueInput` - Create `NodeArrayInput` - Create `NodeStringInput` - Create `NodeNumberInput` - Create `NodeBooleanInput` - Create `NodeFallbackInput` - Create `ClickableInput` from `renderClickableInput(..)` - Amend usage in `CustomNode` - Remove deprecated/unused styling from `flow.css` and `customnode.css` - Fix alignment between `NodeHandle` and `NodeInputField` - Split up `BlockIOSchema` & rename to `BlockIOSubSchema` - Create `BlockIOObjectSubSchema` - Create `BlockIOKVSubSchema` - Create `BlockIOArraySubSchema` - Create `BlockIOStringSubSchema` - Create `BlockIONumberSubSchema` - Create `BlockIOBooleanSubSchema` - Create `BlockIONullSubSchema` - Install `Select` component from shad/cn - refactor(builder): Replace boilerplate `button` styling with `Button` components - refactor(builder): Move `NodeInputField.tsx` to `node-input-components.tsx` --------- Co-authored-by: Krzysztof Czerwinski --- rnd/autogpt_builder/package.json | 1 + .../src/components/CustomEdge.tsx | 5 +- .../src/components/CustomNode.tsx | 166 +++-- rnd/autogpt_builder/src/components/Flow.tsx | 43 +- .../src/components/NodeHandle.tsx | 14 +- .../src/components/NodeInputField.tsx | 357 ---------- .../src/components/SchemaTooltip.tsx | 4 +- .../src/components/customnode.css | 110 +--- .../components/edit/control/BlocksControl.tsx | 6 +- .../components/edit/control/ControlPanel.tsx | 11 +- .../components/edit/control/SaveControl.tsx | 6 +- rnd/autogpt_builder/src/components/flow.css | 35 - .../src/components/node-input-components.tsx | 616 ++++++++++++++++++ .../src/components/ui/select.tsx | 167 +++++ .../src/lib/autogpt-server-api/types.ts | 112 ++-- rnd/autogpt_builder/src/lib/utils.ts | 3 + rnd/autogpt_builder/yarn.lock | 27 + 17 files changed, 1052 insertions(+), 631 deletions(-) delete mode 100644 rnd/autogpt_builder/src/components/NodeInputField.tsx create mode 100644 rnd/autogpt_builder/src/components/node-input-components.tsx create mode 100644 rnd/autogpt_builder/src/components/ui/select.tsx diff --git a/rnd/autogpt_builder/package.json b/rnd/autogpt_builder/package.json index f10491256f..60d051f37a 100644 --- a/rnd/autogpt_builder/package.json +++ b/rnd/autogpt_builder/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", diff --git a/rnd/autogpt_builder/src/components/CustomEdge.tsx b/rnd/autogpt_builder/src/components/CustomEdge.tsx index 25ac6c329f..137fdea8d4 100644 --- a/rnd/autogpt_builder/src/components/CustomEdge.tsx +++ b/rnd/autogpt_builder/src/components/CustomEdge.tsx @@ -32,9 +32,8 @@ const CustomEdgeFC: FC> = ({ const [isHovered, setIsHovered] = useState(false); const { setEdges } = useReactFlow(); - const onEdgeClick = () => { + const onEdgeRemoveClick = () => { setEdges((edges) => edges.filter((edge) => edge.id !== id)); - data.clearNodesStatusAndOutput(); }; const [path, labelX, labelY] = getBezierPath({ @@ -105,7 +104,7 @@ const CustomEdgeFC: FC> = ({ onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} className={`edge-label-button ${isHovered ? "visible" : ""}`} - onClick={onEdgeClick} + onClick={onEdgeRemoveClick} > diff --git a/rnd/autogpt_builder/src/components/CustomNode.tsx b/rnd/autogpt_builder/src/components/CustomNode.tsx index 63aeb222a8..65b4550c21 100644 --- a/rnd/autogpt_builder/src/components/CustomNode.tsx +++ b/rnd/autogpt_builder/src/components/CustomNode.tsx @@ -15,13 +15,15 @@ import { BlockIORootSchema, NodeExecutionResult, } from "@/lib/autogpt-server-api/types"; -import { BlockSchema } from "@/lib/types"; import { beautifyString, setNestedProperty } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; -import NodeHandle from "./NodeHandle"; -import NodeInputField from "./NodeInputField"; import { Copy, Trash2 } from "lucide-react"; import { history } from "./history"; +import NodeHandle from "./NodeHandle"; +import { NodeGenericInputField } from "./node-input-components"; + +type ParsedKey = { key: string; index?: number }; export type CustomNodeData = { blockType: string; @@ -37,8 +39,8 @@ export type CustomNodeData = { targetHandle: string; }>; isOutputOpen: boolean; - status?: string; - output_data?: any; + status?: NodeExecutionResult["status"]; + output_data?: NodeExecutionResult["output_data"]; block_id: string; backend_id?: string; errors?: { [key: string]: string | null }; @@ -110,16 +112,30 @@ const CustomNode: FC> = ({ data, id }) => { )); }; - const handleInputChange = (key: string, value: any) => { - const keys = key.split("."); + const handleInputChange = (path: string, value: any) => { + const keys = parseKeys(path); const newValues = JSON.parse(JSON.stringify(data.hardcodedValues)); let current = newValues; for (let i = 0; i < keys.length - 1; i++) { - if (!current[keys[i]]) current[keys[i]] = {}; - current = current[keys[i]]; + const { key: currentKey, index } = keys[i]; + if (index !== undefined) { + if (!current[currentKey]) current[currentKey] = []; + if (!current[currentKey][index]) current[currentKey][index] = {}; + current = current[currentKey][index]; + } else { + if (!current[currentKey]) current[currentKey] = {}; + current = current[currentKey]; + } + } + + const lastKey = keys[keys.length - 1]; + if (lastKey.index !== undefined) { + if (!current[lastKey.key]) current[lastKey.key] = []; + current[lastKey.key][lastKey.index] = value; + } else { + current[lastKey.key] = value; } - current[keys[keys.length - 1]] = value; console.log(`Updating hardcoded values for node ${id}:`, newValues); @@ -135,16 +151,49 @@ const CustomNode: FC> = ({ data, id }) => { data.setHardcodedValues(newValues); const errors = data.errors || {}; // Remove error with the same key - setNestedProperty(errors, key, null); + setNestedProperty(errors, path, null); data.setErrors({ ...errors }); }; + // Helper function to parse keys with array indices + const parseKeys = (key: string): ParsedKey[] => { + const regex = /(\w+)|\[(\d+)\]/g; + const keys: ParsedKey[] = []; + let match; + let currentKey: string | null = null; + + while ((match = regex.exec(key)) !== null) { + if (match[1]) { + if (currentKey !== null) { + keys.push({ key: currentKey }); + } + currentKey = match[1]; + } else if (match[2]) { + if (currentKey !== null) { + keys.push({ key: currentKey, index: parseInt(match[2], 10) }); + currentKey = null; + } else { + throw new Error("Invalid key format: array index without a key"); + } + } + } + + if (currentKey !== null) { + keys.push({ key: currentKey }); + } + + return keys; + }; + const getValue = (key: string) => { - const keys = key.split("."); - return keys.reduce( - (acc, k) => (acc && acc[k] !== undefined ? acc[k] : ""), - data.hardcodedValues, - ); + const keys = parseKeys(key); + return keys.reduce((acc, k) => { + if (acc === undefined) return undefined; + if (k.index !== undefined) { + return Array.isArray(acc[k.key]) ? acc[k.key][k.index] : undefined; + } + return acc[k.key]; + }, data.hardcodedValues as any); }; const isHandleConnected = (key: string) => { @@ -208,12 +257,10 @@ const CustomNode: FC> = ({ data, id }) => { const handleHovered = () => { setIsHovered(true); - console.log("isHovered", isHovered); }; const handleMouseLeave = () => { setIsHovered(false); - console.log("isHovered", isHovered); }; const deleteNode = useCallback(() => { @@ -274,58 +321,66 @@ const CustomNode: FC> = ({ data, id }) => {
{beautifyString(data.blockType?.replace(/Block$/, "") || data.title)}
-
+
{isHovered && ( <> - - + )}
-
+
{data.inputSchema && - Object.entries(data.inputSchema.properties).map(([key, schema]) => { - const isRequired = data.inputSchema.required?.includes(key); - return ( - (isRequired || isAdvancedOpen) && ( -
{}}> - - {!isHandleConnected(key) && ( - { + const isRequired = data.inputSchema.required?.includes(propKey); + return ( + (isRequired || isAdvancedOpen) && ( +
{}}> + - )} -
- ) - ); - })} + {!isHandleConnected(propKey) && ( + + )} +
+ ) + ); + }, + )}
-
+
{data.outputSchema && generateOutputHandles(data.outputSchema)}
@@ -355,14 +410,11 @@ const CustomNode: FC> = ({ data, id }) => {
)}
- + Output {hasOptionalFields() && ( <> - + Advanced )} diff --git a/rnd/autogpt_builder/src/components/Flow.tsx b/rnd/autogpt_builder/src/components/Flow.tsx index 9441960c7a..25c0bb0a0e 100644 --- a/rnd/autogpt_builder/src/components/Flow.tsx +++ b/rnd/autogpt_builder/src/components/Flow.tsx @@ -23,7 +23,6 @@ import CustomNode, { CustomNodeData } from "./CustomNode"; import "./flow.css"; import AutoGPTServerAPI, { Block, - BlockIOSchema, Graph, NodeExecutionResult, } from "@/lib/autogpt-server-api"; @@ -458,7 +457,6 @@ const FlowEditor: React.FC<{ targetHandle: link.sink_name, })), isOutputOpen: false, - setIsAnyModalOpen: setIsAnyModalOpen, // Pass setIsAnyModalOpen function setErrors: (errors: { [key: string]: string | null }) => { setNodes((nds) => nds.map((node) => @@ -502,11 +500,7 @@ const FlowEditor: React.FC<{ ); } - const prepareNodeInputData = ( - node: Node, - allNodes: Node[], - allEdges: Edge[], - ) => { + const prepareNodeInputData = (node: Node) => { console.log("Preparing input data for node:", node.id, node.data.blockType); const blockSchema = availableNodes.find( @@ -519,7 +513,7 @@ const FlowEditor: React.FC<{ } const getNestedData = ( - schema: BlockIOSchema, + schema: BlockIOSubSchema, values: { [key: string]: any }, ): { [key: string]: any } => { let inputData: { [key: string]: any } = {}; @@ -580,7 +574,7 @@ const FlowEditor: React.FC<{ const key = `${node.data.block_id}_${node.position.x}_${node.position.y}`; blockIdToNodeIdMap[key] = node.id; }); - const inputDefault = prepareNodeInputData(node, nodes, edges); + const inputDefault = prepareNodeInputData(node); const inputNodes = edges .filter((edge) => edge.target === node.id) .map((edge) => ({ @@ -685,7 +679,10 @@ const FlowEditor: React.FC<{ // Populate errors if validation fails validate.errors?.forEach((error) => { // Skip error if there's an edge connected - const path = error.instancePath || error.schemaPath; + const path = + "dataPath" in error + ? (error.dataPath as string) + : error.instancePath; const handle = path.split(/[\/.]/)[0]; if ( node.data.connections.some( @@ -845,17 +842,17 @@ const FlowEditor: React.FC<{ const editorControls: Control[] = [ { label: "Undo", - icon: , + icon: , onClick: handleUndo, }, { label: "Redo", - icon: , + icon: , onClick: handleRedo, }, { label: "Run", - icon: , + icon: , onClick: runAgent, }, ]; @@ -883,17 +880,15 @@ const FlowEditor: React.FC<{ onNodeDragStart={onNodesChangeStart} onNodeDragStop={onNodesChangeEnd} > -
- - - - -
+ + + +
); diff --git a/rnd/autogpt_builder/src/components/NodeHandle.tsx b/rnd/autogpt_builder/src/components/NodeHandle.tsx index a61ac48777..b300929f88 100644 --- a/rnd/autogpt_builder/src/components/NodeHandle.tsx +++ b/rnd/autogpt_builder/src/components/NodeHandle.tsx @@ -1,4 +1,4 @@ -import { BlockIOSchema } from "@/lib/autogpt-server-api/types"; +import { BlockIOSubSchema } 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: BlockIOSchema; + schema: BlockIOSubSchema; isConnected: boolean; isRequired?: boolean; side: "left" | "right"; @@ -28,7 +28,7 @@ const NodeHandle: FC = ({ null: "null", }; - const typeClass = `text-sm ${getTypeTextColor(schema.type)} ${side === "left" ? "text-left" : "text-right"}`; + const typeClass = `text-sm ${getTypeTextColor(schema.type || "any")} ${side === "left" ? "text-left" : "text-right"}`; const label = (
@@ -36,13 +36,13 @@ const NodeHandle: FC = ({ {schema.title || beautifyString(keyName)} {isRequired ? "*" : ""} - {typeName[schema.type]} + {typeName[schema.type] || "any"}
); const dot = (
); @@ -53,7 +53,7 @@ const NodeHandle: FC = ({ type="target" position={Position.Left} id={keyName} - className="group -ml-[29px]" + className="group -ml-[26px]" >
{dot} @@ -70,7 +70,7 @@ const NodeHandle: FC = ({ type="source" position={Position.Right} id={keyName} - className="group -mr-[29px]" + className="group -mr-[26px]" >
{label} diff --git a/rnd/autogpt_builder/src/components/NodeInputField.tsx b/rnd/autogpt_builder/src/components/NodeInputField.tsx deleted file mode 100644 index 566ccecc94..0000000000 --- a/rnd/autogpt_builder/src/components/NodeInputField.tsx +++ /dev/null @@ -1,357 +0,0 @@ -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: BlockIOSchema; - parentKey?: string; - value: string | Array | { [key: string]: string }; - handleInputClick: (key: string) => void; - handleInputChange: (key: string, value: any) => void; - errors?: { [key: string]: string } | string | null; -}; - -const NodeInputField: FC = ({ - 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 [keyValuePairs, _setKeyValuePairs] = useState< - { key: string; value: string }[] - >( - "additionalProperties" in schema && value - ? Object.entries(value).map(([key, value]) => ({ - key: key, - value: value, - })) - : [], - ); - - 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" : ""}`; - - return secret ? ( -
handleInputClick(fullKey)}> - {value ? ( - ******** - ) : ( - {placeholder} - )} -
- ) : ( -
handleInputClick(fullKey)}> - {value || {placeholder}} -
- ); - }; - - if ("properties" in schema) { - return ( -
- {displayKey}: - {Object.entries(schema.properties).map(([propKey, propSchema]) => ( -
- -
- ))} -
- ); - } - - if (schema.type === "object" && schema.additionalProperties) { - return ( -
-
- {keyValuePairs.map(({ key, value }, index) => ( -
- - setKeyValuePairs( - keyValuePairs.toSpliced(index, 1, { - key: e.target.value, - value: value, - }), - ) - } - /> - - setKeyValuePairs( - keyValuePairs.toSpliced(index, 1, { - key: key, - value: e.target.value, - }), - ) - } - /> - -
- ))} - -
- {error && {error}} -
- ); - } - - if ("anyOf" in schema) { - const types = schema.anyOf.map((s) => ("type" in s ? s.type : undefined)); - if (types.includes("string") && types.includes("null")) { - return ( -
- {renderClickableInput( - value as string, - schema.placeholder || `Enter ${displayKey} (optional)`, - )} - {error && {error}} -
- ); - } - } - - if ("allOf" in schema) { - return ( -
- {displayKey}: - {"properties" in schema.allOf[0] && - Object.entries(schema.allOf[0].properties).map( - ([propKey, propSchema]) => ( -
- -
- ), - )} -
- ); - } - - if ("oneOf" in schema) { - return ( -
- {displayKey}: - {"properties" in schema.oneOf[0] && - Object.entries(schema.oneOf[0].properties).map( - ([propKey, propSchema]) => ( -
- -
- ), - )} -
- ); - } - - if (!("type" in schema)) { - console.warn(`Schema for input ${key} does not specify a type:`, schema); - return ( -
- {renderClickableInput( - value as string, - schema.placeholder || `Enter ${beautifyString(displayKey)} (Complex)`, - )} - {error && {error}} -
- ); - } - - switch (schema.type) { - case "string": - if (schema.enum) { - return ( -
- - {error && {error}} -
- ); - } - - if (schema.secret) { - return ( -
- {renderClickableInput( - value as string, - schema.placeholder || `Enter ${displayKey}`, - true, - )} - {error && {error}} -
- ); - } - - return ( -
- {renderClickableInput( - value as string, - schema.placeholder || `Enter ${displayKey}`, - )} - {error && {error}} -
- ); - case "boolean": - return ( -
- - {error && {error}} -
- ); - case "number": - case "integer": - return ( -
- - handleInputChange(fullKey, parseFloat(e.target.value)) - } - className={`number-input ${error ? "border-error" : ""}`} - /> - {error && {error}} -
- ); - case "array": - if (schema.items && schema.items.type === "string") { - const arrayValues = (value as Array) || []; - return ( -
- {arrayValues.map((item: string, index: number) => ( -
- - handleInputChange(`${fullKey}.${index}`, e.target.value) - } - className="array-item-input" - /> - -
- ))} - - {error && {error}} -
- ); - } - return null; - default: - console.warn(`Schema for input ${key} specifies unknown type:`, schema); - return ( -
- {renderClickableInput( - value as string, - schema.placeholder || - `Enter ${beautifyString(displayKey)} (Complex)`, - )} - {error && {error}} -
- ); - } -}; - -export default NodeInputField; diff --git a/rnd/autogpt_builder/src/components/SchemaTooltip.tsx b/rnd/autogpt_builder/src/components/SchemaTooltip.tsx index aa2131a6a3..e16db7508a 100644 --- a/rnd/autogpt_builder/src/components/SchemaTooltip.tsx +++ b/rnd/autogpt_builder/src/components/SchemaTooltip.tsx @@ -4,11 +4,11 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { BlockIOSchema } from "@/lib/autogpt-server-api/types"; +import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types"; import { Info } from "lucide-react"; import ReactMarkdown from "react-markdown"; -const SchemaTooltip: React.FC<{ schema: BlockIOSchema }> = ({ schema }) => { +const SchemaTooltip: React.FC<{ schema: BlockIOSubSchema }> = ({ schema }) => { if (!schema.description) return null; return ( diff --git a/rnd/autogpt_builder/src/components/customnode.css b/rnd/autogpt_builder/src/components/customnode.css index c6968fed73..1dcf75b789 100644 --- a/rnd/autogpt_builder/src/components/customnode.css +++ b/rnd/autogpt_builder/src/components/customnode.css @@ -1,5 +1,5 @@ .custom-node { - padding: 15px; + @apply p-3; border: 3px solid #4b5563; border-radius: 12px; background: #ffffff; @@ -9,13 +9,6 @@ transition: border-color 0.3s ease-in-out; } -.node-content { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 1px; -} - .custom-node .mb-2 { display: flex; justify-content: space-between; @@ -30,45 +23,6 @@ margin-right: 10px; } -.node-actions { - display: flex; - gap: 5px; -} - -.node-action-button { - width: 32px; - /* Increased size */ - height: 32px; - /* Increased size */ - display: flex; - align-items: center; - justify-content: center; - background-color: #f3f4f6; - /* Light gray background */ - border: 1px solid #d1d5db; - /* Light border */ - border-radius: 6px; - color: #4b5563; - transition: all 0.2s ease-in-out; - cursor: pointer; -} - -.node-action-button:hover { - background-color: #e5e7eb; - color: #1f2937; -} - -.node-action-button:focus { - outline: none; - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5); -} - -.node-action-button svg { - width: 18px; - /* Increased icon size */ - height: 18px; - /* Increased icon size */ -} /* Existing styles */ .handle-container { display: flex; @@ -89,38 +43,10 @@ transform: none; } -.input-container { - margin-bottom: 5px; -} - -.clickable-input { - padding: 5px; - width: 325px; - border-radius: 4px; - background: #ffffff; - border: 1px solid #d1d1d1; - color: #000000; - cursor: pointer; - word-break: break-all; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - position: relative; -} - .border-error { border: 1px solid #d9534f; } -.clickable-input span { - display: inline-block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: calc(100% - 100px); - vertical-align: middle; -} - .select-input { width: 100%; padding: 5px; @@ -191,29 +117,9 @@ .error-message { color: #d9534f; - font-size: 12px; + font-size: 13px; margin-top: 5px; -} - -.object-input { - margin-left: 10px; - border-left: 1px solid #000; /* Border for nested inputs */ - padding-left: 10px; -} - -.nested-input { - margin-top: 5px; -} - -.key-value-input { - display: flex; - gap: 5px; - align-items: center; - margin-bottom: 5px; -} - -.key-value-input input { - flex-grow: 1; + margin-left: 5px; } /* Styles for node states */ @@ -240,3 +146,13 @@ .custom-switch { padding-left: 2px; } + +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="number"] { + -moz-appearance: textfield; +} diff --git a/rnd/autogpt_builder/src/components/edit/control/BlocksControl.tsx b/rnd/autogpt_builder/src/components/edit/control/BlocksControl.tsx index 4a834a13c8..637923bc3a 100644 --- a/rnd/autogpt_builder/src/components/edit/control/BlocksControl.tsx +++ b/rnd/autogpt_builder/src/components/edit/control/BlocksControl.tsx @@ -40,8 +40,10 @@ export const BlocksControl: React.FC = ({ return ( - - + + { +export const ControlPanel = ({ + controls, + children, + className, +}: ControlPanelProps) => { return ( -