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

This commit is contained in:
Toran Bruce Richards
2024-08-02 09:38:37 +01:00
committed by GitHub
217 changed files with 18079 additions and 2957 deletions

View File

@@ -23,6 +23,16 @@
# Frontend
!frontend/build/web/
# rnd
!rnd/
# Explicitly re-ignore some folders
.*
**/__pycache__
# rnd
rnd/autogpt_builder/.next/
rnd/autogpt_builder/node_modules
rnd/autogpt_builder/.env.example
rnd/autogpt_builder/.env.local

View File

@@ -88,14 +88,16 @@ body:
- type: dropdown
attributes:
label: Do you use OpenAI GPT-3 or GPT-4?
label: What LLM Provider do you use?
description: >
If you are using AutoGPT with `SMART_LLM=gpt-3.5-turbo`, your problems may be caused by
the [limitations](https://github.com/Significant-Gravitas/AutoGPT/issues?q=is%3Aissue+label%3A%22AI+model+limitation%22) of GPT-3.5.
options:
- GPT-3.5
- GPT-4
- GPT-4(32k)
- Azure
- Groq
- Anthropic
- Llamafile
- Other (detail in issue)
validations:
required: true
@@ -126,6 +128,13 @@ body:
label: Specify the area
description: Please specify the area you think is best related to the issue.
- type: input
attributes:
label: What commit or version are you using?
description: It is helpful for us to reproduce to know what version of the software you were using when this happened. Please run `git log -n 1 --pretty=format:"%H"` to output the full commit hash.
validations:
required: true
- type: textarea
attributes:
label: Describe your issue.

8
.github/labeler.yml vendored
View File

@@ -17,3 +17,11 @@ Frontend:
documentation:
- changed-files:
- any-glob-to-any-file: docs/**
Builder:
- changed-files:
- any-glob-to-any-file: rnd/autogpt_builder/**
Server:
- changed-files:
- any-glob-to-any-file: rnd/autogpt_server/**

56
.github/workflows/autogpt-infra-ci.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: AutoGPT Builder Infra
on:
push:
branches: [ master ]
paths:
- '.github/workflows/autogpt-infra-ci.yml'
- 'rnd/infra/**'
pull_request:
paths:
- '.github/workflows/autogpt-infra-ci.yml'
- 'rnd/infra/**'
defaults:
run:
shell: bash
working-directory: rnd/infra
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: TFLint
uses: pauloconnor/tflint-action@v0.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tflint_path: terraform/
tflint_recurse: true
tflint_changed_only: false
- name: Set up Helm
uses: azure/setup-helm@v4.2.0
with:
version: v3.14.4
- name: Set up chart-testing
uses: helm/chart-testing-action@v2.6.0
- name: Run chart-testing (list-changed)
id: list-changed
run: |
changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }})
if [[ -n "$changed" ]]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Run chart-testing (lint)
if: steps.list-changed.outputs.changed == 'true'
run: ct lint --target-branch ${{ github.event.repository.default_branch }}

View File

@@ -31,9 +31,20 @@ jobs:
matrix:
python-version: ["3.10"]
platform-os: [ubuntu, macos, macos-arm64, windows]
db-platform: [postgres, sqlite]
runs-on: ${{ matrix.platform-os != 'macos-arm64' && format('{0}-latest', matrix.platform-os) || 'macos-14' }}
steps:
- name: Setup PostgreSQL
if: matrix.db-platform == 'postgres'
uses: ikalnytskyi/action-setup-postgres@v6
with:
username: ${{ secrets.DB_USER }}
password: ${{ secrets.DB_PASS }}
database: postgres
port: 5432
id: postgres
# Quite slow on macOS (2~4 minutes to set up Docker)
# - name: Set up Docker (macOS)
# if: runner.os == 'macOS'
@@ -105,162 +116,45 @@ jobs:
- name: Install Python dependencies
run: poetry install
- name: Generate Prisma Client
- name: Generate Prisma Client (Postgres)
if: matrix.db-platform == 'postgres'
run: poetry run prisma generate --schema postgres/schema.prisma
- name: Run Database Migrations (Postgres)
if: matrix.db-platform == 'postgres'
run: poetry run prisma migrate dev --schema postgres/schema.prisma --name updates
env:
CONNECTION_STR: ${{ steps.postgres.outputs.connection-uri }}
- name: Generate Prisma Client (SQLite)
if: matrix.db-platform == 'sqlite'
run: poetry run prisma generate
- name: Run Database Migrations
- name: Run Database Migrations (SQLite)
if: matrix.db-platform == 'sqlite'
run: poetry run prisma migrate dev --name updates
- name: Run Linter
run: poetry run lint
- name: Run pytest with coverage
run: |
poetry run pytest -vv \
test
env:
CI: true
PLAIN_OUTPUT: True
env:
CI: true
PLAIN_OUTPUT: True
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_NAME: postgres
DB_PORT: 5432
RUN_ENV: local
PORT: 8080
DATABASE_URL: postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASS }}@localhost:5432/${{ secrets.DB_NAME }}
# - name: Upload coverage reports to Codecov
# uses: codecov/codecov-action@v4
# with:
# token: ${{ secrets.CODECOV_TOKEN }}
# flags: autogpt-server,${{ runner.os }}
build:
permissions:
contents: read
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
python-version: ["3.10"]
platform-os: [ubuntu, macos, macos-arm64, windows]
runs-on: ${{ matrix.platform-os != 'macos-arm64' && format('{0}-latest', matrix.platform-os) || 'macos-14' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- id: get_date
name: Get date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Set up Python dependency cache
# On Windows, unpacking cached dependencies takes longer than just installing them
if: runner.os != 'Windows'
uses: actions/cache@v4
with:
path: ${{ runner.os == 'macOS' && '~/Library/Caches/pypoetry' || '~/.cache/pypoetry' }}
key: poetry-${{ runner.os }}-${{ hashFiles('rnd/autogpt_server/poetry.lock') }}
- name: Install Poetry (Unix)
if: runner.os != 'Windows'
run: |
curl -sSL https://install.python-poetry.org | python3 -
if [ "${{ runner.os }}" = "macOS" ]; then
PATH="$HOME/.local/bin:$PATH"
echo "$HOME/.local/bin" >> $GITHUB_PATH
fi
- name: Install Poetry (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -
$env:PATH += ";$env:APPDATA\Python\Scripts"
echo "$env:APPDATA\Python\Scripts" >> $env:GITHUB_PATH
- name: Install Python dependencies
run: poetry install
- name: Generate Prisma Client
run: poetry run prisma generate
- name: Run Database Migrations
run: poetry run prisma migrate dev --name updates
- name: install rpm
if: matrix.platform-os == 'ubuntu'
run: sudo apt-get install -y alien fakeroot rpm
- name: Build distribution
run: |
case "${{ matrix.platform-os }}" in
"macos" | "macos-arm64")
${MAC_COMMAND}
;;
"windows")
${WINDOWS_COMMAND}
;;
*)
${LINUX_COMMAND}
;;
esac
env:
MAC_COMMAND: "poetry run poe dist_dmg"
WINDOWS_COMMAND: "poetry run poe dist_msi"
LINUX_COMMAND: "poetry run poe dist_appimage"
# break this into seperate steps each with their own name that matches the file
- name: Upload App artifact
uses: actions/upload-artifact@v4
with:
name: autogptserver-app-${{ matrix.platform-os }}
path: /Users/runner/work/AutoGPT/AutoGPT/rnd/autogpt_server/build/*.app
- name: Upload dmg artifact
uses: actions/upload-artifact@v4
with:
name: autogptserver-dmg-${{ matrix.platform-os }}
path: /Users/runner/work/AutoGPT/AutoGPT/rnd/autogpt_server/build/AutoGPTServer.dmg
- name: Upload msi artifact
uses: actions/upload-artifact@v4
with:
name: autogptserver-msi-${{ matrix.platform-os }}
path: D:\a\AutoGPT\AutoGPT\rnd\autogpt_server\dist\*.msi
- name: Upload deb artifact
uses: actions/upload-artifact@v4
with:
name: autogptserver-deb-${{ matrix.platform-os }}
path: /Users/runner/work/AutoGPT/AutoGPT/rnd/autogpt_server/build/*.deb
- name: Upload rpm artifact
uses: actions/upload-artifact@v4
with:
name: autogptserver-rpm-${{ matrix.platform-os }}
path: /Users/runner/work/AutoGPT/AutoGPT/rnd/autogpt_server/build/*.rpm
- name: Upload tar.gz artifact
uses: actions/upload-artifact@v4
with:
name: autogptserver-tar.gz-${{ matrix.platform-os }}
path: /Users/runner/work/AutoGPT/AutoGPT/rnd/autogpt_server/build/*.tar.gz
- name: Upload zip artifact
uses: actions/upload-artifact@v4
with:
name: autogptserver-zip-${{ matrix.platform-os }}
path: /Users/runner/work/AutoGPT/AutoGPT/rnd/autogpt_server/build/*.zip
- name: Upload pkg artifact
uses: actions/upload-artifact@v4
with:
name: autogptserver-pkg-${{ matrix.platform-os }}
path: /Users/runner/work/AutoGPT/AutoGPT/rnd/autogpt_server/build/*.pkg
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: autogptserver-AppImage-${{ matrix.platform-os }}
path: /Users/runner/work/AutoGPT/AutoGPT/rnd/autogpt_server/dist/*.AppImage

View File

@@ -0,0 +1,55 @@
import os
import requests
import sys
# GitHub API endpoint
api_url = os.environ["GITHUB_API_URL"]
repo = os.environ["GITHUB_REPOSITORY"]
sha = os.environ["GITHUB_SHA"]
# GitHub token for authentication
github_token = os.environ["GITHUB_TOKEN"]
# API endpoint for check runs for the specific SHA
endpoint = f"{api_url}/repos/{repo}/commits/{sha}/check-runs"
# Set up headers for authentication
headers = {
"Authorization": f"token {github_token}",
"Accept": "application/vnd.github.v3+json"
}
# Make the API request
response = requests.get(endpoint, headers=headers)
if response.status_code != 200:
print(f"Error: Unable to fetch check runs data. Status code: {response.status_code}")
sys.exit(1)
check_runs = response.json()["check_runs"]
# Flag to track if all other check runs have passed
all_others_passed = True
# Current run id
current_run_id = os.environ["GITHUB_RUN_ID"]
for run in check_runs:
if str(run["id"]) != current_run_id:
status = run["status"]
conclusion = run["conclusion"]
if status == "completed":
if conclusion not in ["success", "skipped", "neutral"]:
all_others_passed = False
print(f"Check run {run['name']} (ID: {run['id']}) has conclusion: {conclusion}")
else:
print(f"Check run {run['name']} (ID: {run['id']}) is still {status}.")
all_others_passed = False
if all_others_passed:
print("All other completed check runs have passed. This check passes.")
sys.exit(0)
else:
print("Some check runs have failed or have not completed. This check fails.")
sys.exit(1)

51
.github/workflows/workflow-checker.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: PR Status Checker
on:
workflow_run:
workflows: ["*"]
types:
- completed
jobs:
status-check:
name: Check Actions Status
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Debug Information
run: |
echo "Event name: ${{ github.event_name }}"
echo "Workflow: ${{ github.workflow }}"
echo "Action: ${{ github.action }}"
echo "Actor: ${{ github.actor }}"
echo "Repository: ${{ github.repository }}"
echo "Ref: ${{ github.ref }}"
echo "Head ref: ${{ github.head_ref }}"
echo "Base ref: ${{ github.base_ref }}"
echo "Event payload:"
cat $GITHUB_EVENT_PATH
- name: Debug File Structure
run: |
echo "Current directory:"
pwd
echo "Directory contents:"
ls -R
echo "GitHub workspace:"
echo $GITHUB_WORKSPACE
echo "GitHub workspace contents:"
ls -R $GITHUB_WORKSPACE
- name: Check Actions Status
run: |
echo "Current directory before running Python script:"
pwd
echo "Attempting to run Python script:"
python .github/scripts/check_actions_status.py
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -33,7 +33,9 @@
"path": ".."
}
],
"settings": {},
"settings": {
"python.analysis.typeCheckingMode": "basic",
},
"extensions": {
"recommendations": [
"charliermarsh.ruff",

View File

@@ -1,17 +1,43 @@
# AutoGPT: build & use AI agents
# AutoGPT: Build & Use AI Agents
[![Discord Follow](https://dcbadge.vercel.app/api/server/autogpt?style=flat)](https://discord.gg/autogpt)  
[![Twitter Follow](https://img.shields.io/twitter/follow/Auto_GPT?style=social)](https://twitter.com/Auto_GPT)  
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
**AutoGPT** is a generalist LLM based AI agent that can autonomously accomplish minor tasks.
**AutoGPT** is a powerful tool that lets you create and run intelligent agents. These agents can perform various tasks automatically, making your life easier.
**Examples**:
## How to Get Started
- Look up and summarize this research paper
- Write a marketing for food supplements
- Write a blog post detailing the news in AI
https://github.com/user-attachments/assets/8508f4dc-b362-4cab-900f-644964a96cdf
### 🧱 AutoGPT Builder
The AutoGPT Builder is the frontend. It allows you to design agents using an easy flowchart style. You build your agent by connecting blocks, where each block performs a single action. It's simple and intuitive!
[Read this guide](https://docs.agpt.co/server/new_blocks/) to learn how to build your own custom blocks.
### 💽 AutoGPT Server
The AutoGPT Server is the backend. This is where your agents run. Once deployed, agents can be triggered by external sources and can operate continuously.
### 🐙 Example Agents
Here are two examples of what you can do with AutoGPT:
1. **Reddit Marketing Agent**
- This agent reads comments on Reddit.
- It looks for people asking about your product.
- It then automatically responds to them.
2. **YouTube Content Repurposing Agent**
- This agent subscribes to your YouTube channel.
- When you post a new video, it transcribes it.
- It uses AI to write a search engine optimized blog post.
- Then, it publishes this blog post to your Medium account.
These examples show just a glimpse of what you can achieve with AutoGPT!
---
Our mission is to provide the tools, so that you can focus on what matters:
- 🏗️ **Building** - Lay the foundation for something amazing.
@@ -23,11 +49,13 @@ Be part of the revolution! **AutoGPT** is here to stay, at the forefront of AI i
**📖 [Documentation](https://docs.agpt.co)**
 | 
**🚀 [Contributing](CONTRIBUTING.md)**
 | 
---
## 🤖 AutoGPT Classic
> Below is information about the classic version of AutoGPT.
**🛠️ [Build your own Agent - Quickstart](FORGE-QUICKSTART.md)**
## 🧱 Building blocks
### 🏗️ Forge
**Forge your own agent!** – Forge is a ready-to-go template for your agent application. All the boilerplate code is already handled, letting you channel all your creativity into the things that set *your* agent apart. All tutorials are located [here](https://medium.com/@aiedge/autogpt-forge-e3de53cc58ec). Components from the [`forge.sdk`](/forge/forge/sdk) can also be used individually to speed up development and reduce boilerplate in your agent project.

View File

@@ -11,6 +11,9 @@
## GROQ_API_KEY - Groq API Key (Example: gsk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
# GROQ_API_KEY=
## LLAMAFILE_API_BASE - Llamafile API base URL
# LLAMAFILE_API_BASE=http://localhost:8080/v1
## TELEMETRY_OPT_IN - Share telemetry on errors and other issues with the AutoGPT team, e.g. through Sentry.
## This helps us to spot and solve problems earlier & faster. (Default: DISABLED)
# TELEMETRY_OPT_IN=true
@@ -102,6 +105,7 @@
## HUGGINGFACE_API_TOKEN - HuggingFace API token (Default: None)
# HUGGINGFACE_API_TOKEN=
### Stable Diffusion (IMAGE_PROVIDER=sdwebui)
## SD_WEBUI_AUTH - Stable Diffusion Web UI username:password pair (Default: None)

3
autogpt/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"python.analysis.typeCheckingMode": "basic",
}

View File

@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
class AgentProfileGeneratorConfiguration(SystemConfiguration):
model_classification: LanguageModelClassification = UserConfigurable(
llm_classification: LanguageModelClassification = UserConfigurable(
default=LanguageModelClassification.SMART_MODEL
)
_example_call: object = {
@@ -148,12 +148,12 @@ class AgentProfileGenerator(PromptStrategy):
def __init__(
self,
model_classification: LanguageModelClassification,
llm_classification: LanguageModelClassification,
system_prompt: str,
user_prompt_template: str,
create_agent_function: dict,
):
self._model_classification = model_classification
self._llm_classification = llm_classification
self._system_prompt_message = system_prompt
self._user_prompt_template = user_prompt_template
self._create_agent_function = CompletionModelFunction.model_validate(
@@ -161,8 +161,8 @@ class AgentProfileGenerator(PromptStrategy):
)
@property
def model_classification(self) -> LanguageModelClassification:
return self._model_classification
def llm_classification(self) -> LanguageModelClassification:
return self._llm_classification
def build_prompt(self, user_objective: str = "", **kwargs) -> ChatPrompt:
system_message = ChatMessage.system(self._system_prompt_message)

View File

@@ -119,7 +119,7 @@ class Agent(BaseAgent[OneShotAgentActionProposal], Configurable[AgentSettings]):
lambda x: self.llm_provider.count_tokens(x, self.llm.name),
llm_provider,
ActionHistoryConfiguration(
model_name=app_config.fast_llm, max_tokens=self.send_token_limit
llm_name=app_config.fast_llm, max_tokens=self.send_token_limit
),
)
.run_after(WatchdogComponent)
@@ -174,13 +174,19 @@ class Agent(BaseAgent[OneShotAgentActionProposal], Configurable[AgentSettings]):
# Get messages
messages = await self.run_pipeline(MessageProvider.get_messages)
include_os_info = (
self.code_executor.config.execute_local_commands
if hasattr(self, "code_executor")
else False
)
prompt: ChatPrompt = self.prompt_strategy.build_prompt(
messages=messages,
task=self.state.task,
ai_profile=self.state.ai_profile,
ai_directives=directives,
commands=function_specs_from_commands(self.commands),
include_os_info=self.code_executor.config.execute_local_commands,
include_os_info=include_os_info,
)
logger.debug(f"Executing prompt:\n{dump_prompt(prompt)}")

View File

@@ -100,7 +100,7 @@ class OneShotAgentPromptStrategy(PromptStrategy):
self.logger = logger
@property
def model_classification(self) -> LanguageModelClassification:
def llm_classification(self) -> LanguageModelClassification:
return LanguageModelClassification.FAST_MODEL # FIXME: dynamic switching
def build_prompt(

View File

@@ -51,8 +51,9 @@ async def apply_overrides_to_config(
raise click.UsageError("--continuous-limit can only be used with --continuous")
# Check availability of configured LLMs; fallback to other LLM if unavailable
config.fast_llm = await check_model(config.fast_llm, "fast_llm")
config.smart_llm = await check_model(config.smart_llm, "smart_llm")
config.fast_llm, config.smart_llm = await check_models(
(config.fast_llm, "fast_llm"), (config.smart_llm, "smart_llm")
)
if skip_reprompt:
config.skip_reprompt = True
@@ -61,17 +62,22 @@ async def apply_overrides_to_config(
config.skip_news = True
async def check_model(
model_name: ModelName, model_type: Literal["smart_llm", "fast_llm"]
) -> ModelName:
async def check_models(
*models: tuple[ModelName, Literal["smart_llm", "fast_llm"]]
) -> tuple[ModelName, ...]:
"""Check if model is available for use. If not, return gpt-3.5-turbo."""
multi_provider = MultiProvider()
models = await multi_provider.get_available_chat_models()
available_models = await multi_provider.get_available_chat_models()
if any(model_name == m.name for m in models):
return model_name
checked_models: list[ModelName] = []
for model, model_type in models:
if any(model == m.name for m in available_models):
checked_models.append(model)
else:
logger.warning(
f"You don't have access to {model}. "
f"Setting {model_type} to {GPT_3_MODEL}."
)
checked_models.append(GPT_3_MODEL)
logger.warning(
f"You don't have access to {model_name}. Setting {model_type} to {GPT_3_MODEL}."
)
return GPT_3_MODEL
return tuple(checked_models)

5
autogpt/poetry.lock generated
View File

@@ -327,6 +327,7 @@ gTTS = "^2.3.1"
jinja2 = "^3.1.2"
jsonschema = "*"
litellm = "^1.17.9"
numpy = ">=1.26.0,<2.0.0"
openai = "^1.7.2"
Pillow = "*"
playsound = "~1.2.2"
@@ -345,12 +346,12 @@ sqlalchemy = "^2.0.19"
tenacity = "^8.2.2"
tiktoken = ">=0.7.0,<1.0.0"
toml = "^0.10.2"
uvicorn = ">=0.23.2,<1"
uvicorn = {version = ">=0.23.2,<1", extras = ["standard"]}
watchdog = "4.0.0"
webdriver-manager = "^4.0.1"
[package.extras]
benchmark = ["agbenchmark @ file:///home/reinier/code/agpt/AutoGPT/benchmark"]
benchmark = ["agbenchmark @ file:///Users/czerwinski/Projects/AutoGPT/benchmark"]
[package.source]
type = "directory"

3
autogpt/scripts/llamafile/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*.llamafile
*.llamafile.exe
llamafile.exe

View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""
Use llamafile to serve a (quantized) mistral-7b-instruct-v0.2 model
Usage:
cd <repo-root>/autogpt
./scripts/llamafile/serve.py
"""
import os
import platform
import subprocess
from pathlib import Path
from typing import Optional
import click
LLAMAFILE = Path("mistral-7b-instruct-v0.2.Q5_K_M.llamafile")
LLAMAFILE_URL = f"https://huggingface.co/jartine/Mistral-7B-Instruct-v0.2-llamafile/resolve/main/{LLAMAFILE.name}" # noqa
LLAMAFILE_EXE = Path("llamafile.exe")
LLAMAFILE_EXE_URL = "https://github.com/Mozilla-Ocho/llamafile/releases/download/0.8.6/llamafile-0.8.6" # noqa
@click.command()
@click.option(
"--llamafile",
type=click.Path(dir_okay=False, path_type=Path),
help=f"Name of the llamafile to serve. Default: {LLAMAFILE.name}",
)
@click.option("--llamafile_url", help="Download URL for the llamafile you want to use")
@click.option(
"--host", help="Specify the address for the llamafile server to listen on"
)
@click.option(
"--port", type=int, help="Specify the port for the llamafile server to listen on"
)
@click.option(
"--force-gpu",
is_flag=True,
hidden=platform.system() != "Darwin",
help="Run the model using only the GPU (AMD or Nvidia). "
"Otherwise, both CPU and GPU may be (partially) used.",
)
def main(
llamafile: Optional[Path] = None,
llamafile_url: Optional[str] = None,
host: Optional[str] = None,
port: Optional[int] = None,
force_gpu: bool = False,
):
print(f"type(llamafile) = {type(llamafile)}")
if not llamafile:
if not llamafile_url:
llamafile = LLAMAFILE
else:
llamafile = Path(llamafile_url.rsplit("/", 1)[1])
if llamafile.suffix != ".llamafile":
click.echo(
click.style(
"The given URL does not end with '.llamafile' -> "
"can't get filename from URL. "
"Specify the filename using --llamafile.",
fg="red",
),
err=True,
)
return
if llamafile == LLAMAFILE and not llamafile_url:
llamafile_url = LLAMAFILE_URL
elif llamafile_url != LLAMAFILE_URL:
if not click.prompt(
click.style(
"You seem to have specified a different URL for the default model "
f"({llamafile.name}). Are you sure this is correct? "
"If you want to use a different model, also specify --llamafile.",
fg="yellow",
),
type=bool,
):
return
# Go to autogpt/scripts/llamafile/
os.chdir(Path(__file__).resolve().parent)
on_windows = platform.system() == "Windows"
if not llamafile.is_file():
if not llamafile_url:
click.echo(
click.style(
"Please use --lamafile_url to specify a download URL for "
f"'{llamafile.name}'. "
"This will only be necessary once, so we can download the model.",
fg="red",
),
err=True,
)
return
download_file(llamafile_url, llamafile)
if not on_windows:
llamafile.chmod(0o755)
subprocess.run([llamafile, "--version"], check=True)
if not on_windows:
base_command = [f"./{llamafile}"]
else:
# Windows does not allow executables over 4GB, so we have to download a
# model-less llamafile.exe and run that instead.
if not LLAMAFILE_EXE.is_file():
download_file(LLAMAFILE_EXE_URL, LLAMAFILE_EXE)
LLAMAFILE_EXE.chmod(0o755)
subprocess.run([f".\\{LLAMAFILE_EXE}", "--version"], check=True)
base_command = [f".\\{LLAMAFILE_EXE}", "-m", llamafile]
if host:
base_command.extend(["--host", host])
if port:
base_command.extend(["--port", str(port)])
if force_gpu:
base_command.extend(["-ngl", "9999"])
subprocess.run(
[
*base_command,
"--server",
"--nobrowser",
"--ctx-size",
"0",
"--n-predict",
"1024",
],
check=True,
)
# note: --ctx-size 0 means the prompt context size will be set directly from the
# underlying model configuration. This may cause slow response times or consume
# a lot of memory.
def download_file(url: str, to_file: Path) -> None:
print(f"Downloading {to_file.name}...")
import urllib.request
urllib.request.urlretrieve(url, to_file, reporthook=report_download_progress)
print()
def report_download_progress(chunk_number: int, chunk_size: int, total_size: int):
if total_size != -1:
downloaded_size = chunk_number * chunk_size
percent = min(1, downloaded_size / total_size)
bar = "#" * int(40 * percent)
print(
f"\rDownloading: [{bar:<40}] {percent:.0%}"
f" - {downloaded_size/1e6:.1f}/{total_size/1e6:.1f} MB",
end="",
)
if __name__ == "__main__":
main()

View File

@@ -2,5 +2,5 @@
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.formatting.provider": "none"
"python.analysis.typeCheckingMode": "basic",
}

View File

@@ -9,7 +9,7 @@ You can set configuration variables via the `.env` file. If you don't have a `.e
- `ANTHROPIC_API_KEY`: Set this if you want to use Anthropic models with AutoGPT
- `AZURE_CONFIG_FILE`: Location of the Azure Config file relative to the AutoGPT root directory. Default: azure.yaml
- `COMPONENT_CONFIG_FILE`: Path to the component configuration file (json) for an agent. Optional
- `DISABLED_COMMANDS`: Commands to disable. Use comma separated names of commands. See the list of commands from built-in components [here](../components/components.md). Default: None
- `DISABLED_COMMANDS`: Commands to disable. Use comma separated names of commands. See the list of commands from built-in components [here](../../forge/components/components.md). Default: None
- `ELEVENLABS_API_KEY`: ElevenLabs API Key. Optional.
- `ELEVENLABS_VOICE_ID`: ElevenLabs Voice ID. Optional.
- `EMBEDDING_MODEL`: LLM Model to use for embedding tasks. Default: `text-embedding-3-small`
@@ -22,6 +22,7 @@ You can set configuration variables via the `.env` file. If you don't have a `.e
- `GROQ_API_KEY`: Set this if you want to use Groq models with AutoGPT
- `HUGGINGFACE_API_TOKEN`: HuggingFace API, to be used for both image generation and audio to text. Optional.
- `HUGGINGFACE_IMAGE_MODEL`: HuggingFace model to use for image generation. Default: CompVis/stable-diffusion-v1-4
- `LLAMAFILE_API_BASE`: Llamafile API base URL. Default: `http://localhost:8080/v1`
- `OPENAI_API_KEY`: Set this if you want to use OpenAI models; [OpenAI API Key](https://platform.openai.com/account/api-keys).
- `OPENAI_ORGANIZATION`: Organization ID in OpenAI. Optional.
- `PLAIN_OUTPUT`: Plain output, which disables the spinner. Default: False

View File

@@ -198,3 +198,66 @@ If you don't know which to choose, you can safely go with OpenAI*.
[groq/api-keys]: https://console.groq.com/keys
[groq/models]: https://console.groq.com/docs/models
### Llamafile
With llamafile you can run models locally, which means no need to set up billing,
and guaranteed data privacy.
For more information and in-depth documentation, check out the [llamafile documentation].
!!! warning
At the moment, llamafile only serves one model at a time. This means you can not
set `SMART_LLM` and `FAST_LLM` to two different llamafile models.
!!! warning
Due to the issues linked below, llamafiles don't work on WSL. To use a llamafile
with AutoGPT in WSL, you will have to run the llamafile in Windows (outside WSL).
<details>
<summary>Instructions</summary>
1. Get the `llamafile/serve.py` script through one of these two ways:
1. Clone the AutoGPT repo somewhere in your Windows environment,
with the script located at `autogpt/scripts/llamafile/serve.py`
2. Download just the [serve.py] script somewhere in your Windows environment
2. Make sure you have `click` installed: `pip install click`
3. Run `ip route | grep default | awk '{print $3}'` *inside WSL* to get the address
of the WSL host machine
4. Run `python3 serve.py --host {WSL_HOST_ADDR}`, where `{WSL_HOST_ADDR}`
is the address you found at step 3.
If port 8080 is taken, also specify a different port using `--port {PORT}`.
5. In WSL, set `LLAMAFILE_API_BASE=http://{WSL_HOST_ADDR}:8080/v1` in your `.env`.
6. Follow the rest of the regular instructions below.
[serve.py]: https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt/scripts/llamafile/serve.py
</details>
* [Mozilla-Ocho/llamafile#356](https://github.com/Mozilla-Ocho/llamafile/issues/356)
* [Mozilla-Ocho/llamafile#100](https://github.com/Mozilla-Ocho/llamafile/issues/100)
!!! note
These instructions will download and use `mistral-7b-instruct-v0.2.Q5_K_M.llamafile`.
`mistral-7b-instruct-v0.2` is currently the only tested and supported model.
If you want to try other models, you'll have to add them to `LlamafileModelName` in
[`llamafile.py`][forge/llamafile.py].
For optimal results, you may also have to add some logic to adapt the message format,
like `LlamafileProvider._adapt_chat_messages_for_mistral_instruct(..)` does.
1. Run the llamafile serve script:
```shell
python3 ./scripts/llamafile/serve.py
```
The first time this is run, it will download a file containing the model + runtime,
which may take a while and a few gigabytes of disk space.
To force GPU acceleration, add `--use-gpu` to the command.
3. In `.env`, set `SMART_LLM`/`FAST_LLM` or both to `mistral-7b-instruct-v0.2`
4. If the server is running on different address than `http://localhost:8080/v1`,
set `LLAMAFILE_API_BASE` in `.env` to the right base URL
[llamafile documentation]: https://github.com/Mozilla-Ocho/llamafile#readme
[forge/llamafile.py]: https://github.com/Significant-Gravitas/AutoGPT/blob/master/forge/forge/llm/providers/llamafile/llamafile.py

View File

@@ -213,5 +213,5 @@ For example, to disable python coding features, set it to the value below:
DISABLED_COMMANDS=execute_python_code,execute_python_file
```
[components]: ./components/components.md
[commands]: ./components/built-in-components.md
[components]: ../forge/components/components.md
[commands]: ../forge/components/built-in-components.md

View File

@@ -40,7 +40,7 @@ Necessary for saving and loading agent's state (preserving session).
| Config variable | Details | Type | Default |
| ---------------- | -------------------------------------- | ----- | ---------------------------------- |
| `storage_path` | Path to agent files, e.g. state | `str` | `agents/{agent_id}/`[^1] |
| `storage_path` | Path to agent files, e.g. state | `str` | `agents/{agent_id}/`[^1] |
| `workspace_path` | Path to files that agent has access to | `str` | `agents/{agent_id}/workspace/`[^1] |
[^1] This option is set dynamically during component construction as opposed to by default inside the configuration model, `{agent_id}` is replaced with the agent's unique identifier.
@@ -84,7 +84,7 @@ Keeps track of agent's actions and their outcomes. Provides their summary to the
| Config variable | Details | Type | Default |
| ---------------------- | ------------------------------------------------------- | ----------- | ------------------ |
| `model_name` | Name of the llm model used to compress the history | `ModelName` | `"gpt-3.5-turbo"` |
| `llm_name` | Name of the llm model used to compress the history | `ModelName` | `"gpt-3.5-turbo"` |
| `max_tokens` | Maximum number of tokens to use for the history summary | `int` | `1024` |
| `spacy_language_model` | Language model used for summary chunking using spacy | `str` | `"en_core_web_sm"` |
| `full_message_count` | Number of cycles to include unsummarized in the prompt | `int` | `4` |
@@ -155,11 +155,12 @@ Allows agent to search the web. Google credentials aren't required for DuckDuckG
### `WebSearchConfiguration`
| Config variable | Details | Type | Default |
| -------------------------------- | ----------------------------------------------------------------------- | ----- | ------- |
| `google_api_key` | Google API key, *ENV:* `GOOGLE_API_KEY` | `str` | `None` |
| `google_custom_search_engine_id` | Google Custom Search Engine ID, *ENV:* `GOOGLE_CUSTOM_SEARCH_ENGINE_ID` | `str` | `None` |
| `duckduckgo_max_attempts` | Maximum number of attempts to search using DuckDuckGo | `int` | `3` |
| Config variable | Details | Type | Default |
| -------------------------------- | ----------------------------------------------------------------------- | --------------------------- | ------- |
| `google_api_key` | Google API key, *ENV:* `GOOGLE_API_KEY` | `str` | `None` |
| `google_custom_search_engine_id` | Google Custom Search Engine ID, *ENV:* `GOOGLE_CUSTOM_SEARCH_ENGINE_ID` | `str` | `None` |
| `duckduckgo_max_attempts` | Maximum number of attempts to search using DuckDuckGo | `int` | `3` |
| `duckduckgo_backend` | Backend to be used for DDG sdk | `"api" \| "html" \| "lite"` | `"api"` |
### DirectiveProvider
@@ -178,11 +179,12 @@ Allows agent to read websites using Selenium.
| Config variable | Details | Type | Default |
| ----------------------------- | ------------------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `model_name` | Name of the llm model used to read websites | `ModelName` | `"gpt-3.5-turbo"` |
| `llm_name` | Name of the llm model used to read websites | `ModelName` | `"gpt-3.5-turbo"` |
| `web_browser` | Web browser used by Selenium | `"chrome" \| "firefox" \| "safari" \| "edge"` | `"chrome"` |
| `headless` | Run browser in headless mode | `bool` | `True` |
| `user_agent` | User agent used by the browser | `str` | `"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36"` |
| `browse_spacy_language_model` | Spacy language model used for chunking text | `str` | `"en_core_web_sm"` |
| `selenium_proxy` | Http proxy to use with Selenium | `str` | `None` |
### DirectiveProvider

View File

@@ -4,13 +4,26 @@ Welcome to the AutoGPT Documentation.
The AutoGPT project consists of four main components:
* The [Agent](#agent) &ndash; also known as just "AutoGPT"
* The [Benchmark](#benchmark) &ndash; AKA `agbenchmark`
* The [Forge](#forge)
* The [Frontend](#frontend)
- The [Server](#server) &ndash; known as the "AutoGPT Platform"
- The [Agent](#agent) &ndash; also known as just "AutoGPT"
- The [Benchmark](#benchmark) &ndash; AKA `agbenchmark`
- The [Forge](#forge)
- The [Frontend](#frontend)
To tie these together, we also have a [CLI] at the root of the project.
## 🌐 Server
<!-- Setup, then Advanced, then New Blocks -->
**[📖 Setup](server/setup.md)**
&ensp;|&ensp;
**[📖 Advanced Setup](server/advanced_setup.md)**
&ensp;|&ensp;
**[📖 Making New Blocks](server/new_blocks.md)**
The server is the backbone of the New AutoGPT project. It provides the infrastructure for the agents to run, and the UI for you to interact with them. It integrates with the Forge, Agent, and a bespoke UI to provide a seamless experience.
---
## 🤖 Agent

View File

@@ -0,0 +1,69 @@
# Advanced Setup
The advanced steps below are intended for people with sysadmin experience. If you are not comfortable with these steps, please refer to the [basic setup guide](setup.md).
## Introduction
For the advanced setup, first follow the [basic setup guide](setup.md) to get the server up and running. Once you have the server running, you can follow the steps below to configure the server for your specific needs.
## Configuration
### Setting config via environment variables
The server uses environment variables to store configs. You can set these environment variables in a `.env` file in the root of the project. The `.env` file should look like this:
```bash
# .env
KEY1=value1
KEY2=value2
```
The server will automatically load the `.env` file when it starts. You can also set the environment variables directly in your shell. Refer to your operating system's documentation on how to set environment variables in the current session.
The valid options are listed in `.env.example` in the root of the builder and server directories. You can copy the `.env.example` file to `.env` and modify the values as needed.
```bash
# Copy the .env.example file to .env
cp .env.example .env
```
### Secrets directory
The secret directory is located at `./secrets`. You can store any secrets you need in this directory. The server will automatically load the secrets when it starts.
An example for a secret called `my_secret` would look like this:
```bash
# ./secrets/my_secret
my_secret_value
```
This is useful when running on docker so you can copy the secrets into the container without exposing them in the Dockerfile.
## Database selection
### SQLite
By default, the server uses SQLite as the database. SQLite is a file-based database that is easy to set up and use. However, it is not recommended for production usecases where auth is required because that subsystem requires Postgres.
### PostgreSQL
For production use, it is recommended to use PostgreSQL as the database. You will swap the commands you use to generate and run prisma to the following
```bash
poetry run prisma generate --schema postgres/schema.prisma
```
This will generate the Prisma client for PostgreSQL. You will also need to run the PostgreSQL database in a separate container. You can use the `docker-compose.yml` file in the `rnd` directory to run the PostgreSQL database.
```bash
cd rnd/
docker compose up -d
```
You can then run the migrations from the `autogpt_server` directory.
```bash
cd ../autogpt_server
prisma migrate dev --schema postgres/schema.prisma
```

View File

@@ -0,0 +1,218 @@
# Contributing to AutoGPT Agent Server: Creating and Testing Blocks
This guide will walk you through the process of creating and testing a new block for the AutoGPT Agent Server, using the WikipediaSummaryBlock as an example.
## Understanding Blocks and Testing
Blocks are reusable components that can be connected to form a graph representing an agent's behavior. Each block has inputs, outputs, and a specific function. Proper testing is crucial to ensure blocks work correctly and consistently.
## Creating and Testing a New Block
Follow these steps to create and test a new block:
1. **Create a new Python file** in the `autogpt_server/blocks` directory. Name it descriptively and use snake_case. For example: `get_wikipedia_summary.py`.
2. **Import necessary modules and create a class that inherits from `Block`**. Make sure to include all necessary imports for your block.
Every block should contain the following:
```python
from autogpt_server.data.block import Block, BlockSchema, BlockOutput
```
Example for the Wikipedia summary block:
```python
from autogpt_server.data.block import Block, BlockSchema, BlockOutput
from autogpt_server.utils.get_request import GetRequest
import requests
class WikipediaSummaryBlock(Block, GetRequest):
# Block implementation will go here
```
3. **Define the input and output schemas** using `BlockSchema`. These schemas specify the data structure that the block expects to receive (input) and produce (output).
- The input schema defines the structure of the data the block will process. Each field in the schema represents a required piece of input data.
- The output schema defines the structure of the data the block will return after processing. Each field in the schema represents a piece of output data.
Example:
```python
class Input(BlockSchema):
topic: str # The topic to get the Wikipedia summary for
class Output(BlockSchema):
summary: str # The summary of the topic from Wikipedia
error: str # Any error message if the request fails
```
4. **Implement the `__init__` method, including test data and mocks:**
```python
def __init__(self):
super().__init__(
# Unique ID for the block, used across users for templates
# you can generate this with this python one liner
# print(__import__('uuid').uuid4())
id="h5e7f8g9-1b2c-3d4e-5f6g-7h8i9j0k1l2m",
input_schema=WikipediaSummaryBlock.Input, # Assign input schema
output_schema=WikipediaSummaryBlock.Output, # Assign output schema
# Provide sample input, output and test mock for testing the block
test_input={"topic": "Artificial Intelligence"},
test_output=("summary", "summary content"),
test_mock={"get_request": lambda url, json: {"extract": "summary content"}},
)
```
- `id`: A unique identifier for the block.
- `input_schema` and `output_schema`: Define the structure of the input and output data.
Let's break down the testing components:
- `test_input`: This is a sample input that will be used to test the block. It should be a valid input according to your Input schema.
- `test_output`: This is the expected output when running the block with the `test_input`. It should match your Output schema. For non-deterministic outputs or when you only want to assert the type, you can use Python types instead of specific values. In this example, `("summary", str)` asserts that the output key is "summary" and its value is a string.
- `test_mock`: This is crucial for blocks that make network calls. It provides a mock function that replaces the actual network call during testing.
In this case, we're mocking the `get_request` method to always return a dictionary with an 'extract' key, simulating a successful API response. This allows us to test the block's logic without making actual network requests, which could be slow, unreliable, or rate-limited.
5. **Implement the `run` method with error handling:**, this should contain the main logic of the block:
```python
def run(self, input_data: Input) -> BlockOutput:
try:
topic = input_data.topic
url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{topic}"
response = self.get_request(url, json=True)
yield "summary", response['extract']
except requests.exceptions.HTTPError as http_err:
yield "error", f"HTTP error occurred: {http_err}"
except requests.RequestException as e:
yield "error", f"Request to Wikipedia failed: {e}"
except KeyError as e:
yield "error", f"Error parsing Wikipedia response: {e}"
```
- **Try block**: Contains the main logic to fetch and process the Wikipedia summary.
- **API request**: Send a GET request to the Wikipedia API.
- **Error handling**: Handle various exceptions that might occur during the API request and data processing.
- **Yield**: Use `yield` to output the results.
## Key Points to Remember
- **Unique ID**: Give your block a unique ID in the **init** method.
- **Input and Output Schemas**: Define clear input and output schemas.
- **Error Handling**: Implement error handling in the `run` method.
- **Output Results**: Use `yield` to output results in the `run` method.
- **Testing**: Provide test input and output in the **init** method for automatic testing.
## Understanding the Testing Process
The testing of blocks is handled by `test_block.py`, which does the following:
1. It calls the block with the provided `test_input`.
2. If a `test_mock` is provided, it temporarily replaces the specified methods with the mock functions.
3. It then asserts that the output matches the `test_output`.
For the WikipediaSummaryBlock:
- The test will call the block with the topic "Artificial Intelligence".
- Instead of making a real API call, it will use the mock function, which returns `{"extract": "summary content"}`.
- It will then check if the output key is "summary" and its value is a string.
This approach allows us to test the block's logic comprehensively without relying on external services, while also accommodating non-deterministic outputs.
## Tips for Effective Block Testing
1. **Provide realistic test_input**: Ensure your test input covers typical use cases.
2. **Define appropriate test_output**:
- For deterministic outputs, use specific expected values.
- For non-deterministic outputs or when only the type matters, use Python types (e.g., `str`, `int`, `dict`).
- You can mix specific values and types, e.g., `("key1", str), ("key2", 42)`.
3. **Use test_mock for network calls**: This prevents tests from failing due to network issues or API changes.
4. **Consider omitting test_mock for blocks without external dependencies**: If your block doesn't make network calls or use external resources, you might not need a mock.
5. **Consider edge cases**: Include tests for potential error conditions in your `run` method.
6. **Update tests when changing block behavior**: If you modify your block, ensure the tests are updated accordingly.
By following these steps, you can create new blocks that extend the functionality of the AutoGPT Agent Server.
## Blocks we want to see
Below is a list of blocks that we would like to see implemented in the AutoGPT Agent Server. If you're interested in contributing, feel free to pick one of these blocks or suggest your own by editing [docs/content/server/new_blocks.md](https://github.com/Significant-Gravitas/AutoGPT/edit/master/docs/content/server/new_blocks.md) and opening a pull request.
If you would like to implement one of these blocks, open a pull request and we will start the review process.
### Consumer Services/Platforms
- Google sheets - Read/Append [Read in Progress](https://github.com/Significant-Gravitas/AutoGPT/pull/7521)
- Email - Read/Send with Gmail, Outlook, Yahoo, Proton, etc
- Calendar - Read/Write with Google Calendar, Outlook Calendar, etc
- Home Assistant - Call Service, Get Status
- Dominos - Order Pizza, Track Order
- Uber - Book Ride, Track Ride
- Notion - Create/Read Page, Create/Append/Read DB
- Google drive - read/write/overwrite file/folder
### Social Media
- Twitter - Post, Reply, Get Replies, Get Comments, Get Followers, Get Following, Get Tweets, Get Mentions
- Instagram - Post, Reply, Get Comments, Get Followers, Get Following, Get Posts, Get Mentions, Get Trending Posts
- TikTok - Post, Reply, Get Comments, Get Followers, Get Following, Get Videos, Get Mentions, Get Trending Videos
- LinkedIn - Post, Reply, Get Comments, Get Followers, Get Following, Get Posts, Get Mentions, Get Trending Posts
- YouTube - Transcribe Videos/Shorts, Post Videos/Shorts, Read/Reply/React to Comments, Update Thumbnails, Update Description, Update Tags, Update Titles, Get Views, Get Likes, Get Dislikes, Get Subscribers, Get Comments, Get Shares, Get Watch Time, Get Revenue, Get Trending Videos, Get Top Videos, Get Top Channels
- Reddit - Post, Reply, Get Comments, Get Followers, Get Following, Get Posts, Get Mentions, Get Trending Posts
- Treatwell (and related Platforms) - Book, Cancel, Review, Get Recommendations
- Substack - Read/Subscribe/Unsubscribe, Post/Reply, Get Recommendations
- Discord - Read/Post/Reply, Moderation actions
- GoodReads - Read/Post/Reply, Get Recommendations
### E-commerce
- Airbnb - Book, Cancel, Review, Get Recommendations
- Amazon - Order, Track Order, Return, Review, Get Recommendations
- eBay - Order, Track Order, Return, Review, Get Recommendations
- Upwork - Post Jobs, Hire Freelancer, Review Freelancer, Fire Freelancer
### Business Tools
- External Agents - Call other agents similar to AutoGPT
- Trello - Create/Read/Update/Delete Cards, Lists, Boards
- Jira - Create/Read/Update/Delete Issues, Projects, Boards
- Linear - Create/Read/Update/Delete Issues, Projects, Boards
- Excel - Read/Write/Update/Delete Rows, Columns, Sheets
- Slack - Read/Post/Reply to Messages, Create Channels, Invite Users
- ERPNext - Create/Read/Update/Delete Invoices, Orders, Customers, Products
- Salesforce - Create/Read/Update/Delete Leads, Opportunities, Accounts
- HubSpot - Create/Read/Update/Delete Contacts, Deals, Companies
- Zendesk - Create/Read/Update/Delete Tickets, Users, Organizations
- Odoo - Create/Read/Update/Delete Sales Orders, Invoices, Customers
- Shopify - Create/Read/Update/Delete Products, Orders, Customers
- WooCommerce - Create/Read/Update/Delete Products, Orders, Customers
- Squarespace - Create/Read/Update/Delete Pages, Products, Orders
## Agent Templates we want to see
### Data/Information
- Summarize top news of today, of this week, this month via Apple News or other large media outlets BBC, TechCrunch, hackernews, etc
- Create, read, and summarize substack newsletters or any newsletters (blog writer vs blog reader)
- Get/read/summarize the most viral Twitter, Instagram, TikTok (general social media accounts) of the day, week, month
- Get/Read any LinkedIn posts or profile that mention AI Agents
- Read/Summarize discord (might not be able to do this because you need access)
- Read / Get most read books in a given month, year, etc from GoodReads or Amazon Books, etc
- Get dates for specific shows across all streaming services
- Suggest/Recommend/Get most watched shows in a given month, year, etc across all streaming platforms

View File

@@ -0,0 +1,37 @@
# Running Ollama with AutoGPT
Follow these steps to set up and run Ollama and your AutoGPT project:
1. **Run Ollama**
- Open a terminal
- Execute the following command:
```
ollama run llama3
```
- Leave this terminal running
2. **Run the Backend**
- Open a new terminal
- Navigate to the backend directory in the AutoGPT project:
```
cd rnd/autogpt_server/
```
- Start the backend using Poetry:
```
poetry run app
```
3. **Run the Frontend**
- Open another terminal
- Navigate to the frontend directory in the AutoGPT project:
```
cd rnd/autogpt_builder/
```
- Start the frontend development server:
```
npm run dev
```
4. **Choose the Ollama Model**
- Add LLMBlock in the UI
- Choose the last option in the model selection dropdown

View File

@@ -0,0 +1,102 @@
# Setting up the server
- [Introduction](#introduction)
- [Prerequisites](#prerequisites)
## Introduction
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)
!!! warning
**DO NOT FOLLOW ANY OUTSIDE TUTORIALS AS THEY WILL LIKELY BE OUT OF DATE**
## Prerequisites
To setup the server, you need to have the following installed:
- [Node.js](https://nodejs.org/en/)
- [Python 3.10](https://www.python.org/downloads/)
### Checking if you have Node.js and Python installed
You can check if you have Node.js installed by running the following command:
```bash
node -v
```
You can check if you have Python installed by running the following command:
```bash
python --version
```
Once you have node and python installed, you can proceed to the next step.
### Installing the package managers
In order to install the dependencies, you need to have the appropriate package managers installed.
- Installing Yarn
Yarn is a package manager for Node.js. You can install it by running the following command:
```bash
npm install -g yarn
```
- Installing Poetry
Poetry is a package manager for Python. You can install it by running the following command:
```bash
pip install poetry
```
### Installing the dependencies
Once you have installed Yarn and Poetry, you can run the following command to install the dependencies:
```bash
cd rnd/autogpt_server
poetry install
```
**In another terminal**, run the following command to install the dependencies for the frontend:
```bash
cd rnd/autogpt_builder
yarn install
```
Once you have installed the dependencies, you can proceed to the next step.
### Setting up the database
In order to setup the database, you need to run the following command, in the same terminal you ran the `poetry install` command:
```bash
poetry run prisma migrate deploy
```
### Running the server
To run the server, you can run the following command in the same terminal you ran the `poetry install` command:
```bash
poetry run app
```
In the other terminal, you can run the following command to start the frontend:
```bash
yarn dev
```
### Checking if the server is running
You can check if the server is running by visiting [http://localhost:3000](http://localhost:3000) in your browser.

View File

@@ -5,6 +5,12 @@ docs_dir: content
nav:
- Home: index.md
- The AutoGPT Server 🆕:
- Build your own Blocks: server/new_blocks.md
- Setup: server/setup.md
- Advanced Setup: server/advanced_setup.md
- Using Ollama: server/ollama.md
- AutoGPT Agent:
- Introduction: AutoGPT/index.md
- Setup:
@@ -40,7 +46,7 @@ nav:
- Readme: https://github.com/Significant-Gravitas/AutoGPT/blob/master/frontend/README.md
- Docs: docs/index.md
# - Challenges:
# - Introduction: challenges/introduction.md
# - List of Challenges:

3
forge/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"python.analysis.typeCheckingMode": "basic",
}

View File

@@ -116,7 +116,7 @@ You can set sensitive variables in the `.json` file as well but it's recommended
"github_username": null
},
"ActionHistoryConfiguration": {
"model_name": "gpt-3.5-turbo",
"llm_name": "gpt-3.5-turbo",
"max_tokens": 1024,
"spacy_language_model": "en_core_web_sm"
},
@@ -129,7 +129,7 @@ You can set sensitive variables in the `.json` file as well but it's recommended
"duckduckgo_max_attempts": 3
},
"WebSeleniumConfiguration": {
"model_name": "gpt-3.5-turbo",
"llm_name": "gpt-3.5-turbo",
"web_browser": "chrome",
"headless": true,
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36",

View File

@@ -16,7 +16,7 @@ from .model import ActionResult, AnyProposal, Episode, EpisodicActionHistory
class ActionHistoryConfiguration(BaseModel):
model_name: ModelName = OpenAIModelName.GPT3
llm_name: ModelName = OpenAIModelName.GPT3
"""Name of the llm model used to compress the history"""
max_tokens: int = 1024
"""Maximum number of tokens to use up with generated history messages"""
@@ -97,7 +97,7 @@ class ActionHistoryComponent(
async def after_execute(self, result: ActionResult) -> None:
self.event_history.register_result(result)
await self.event_history.handle_compression(
self.llm_provider, self.config.model_name, self.config.spacy_language_model
self.llm_provider, self.config.llm_name, self.config.spacy_language_model
)
@staticmethod

View File

@@ -1,7 +1,7 @@
import json
import logging
import time
from typing import Iterator, Optional
from typing import Iterator, Literal, Optional
from duckduckgo_search import DDGS
from pydantic import BaseModel, SecretStr
@@ -24,6 +24,7 @@ class WebSearchConfiguration(BaseModel):
None, from_env="GOOGLE_CUSTOM_SEARCH_ENGINE_ID", exclude=True
)
duckduckgo_max_attempts: int = 3
duckduckgo_backend: Literal["api", "html", "lite"] = "api"
class WebSearchComponent(
@@ -89,7 +90,9 @@ class WebSearchComponent(
if not query:
return json.dumps(search_results)
search_results = DDGS().text(query, max_results=num_results)
search_results = DDGS().text(
query, max_results=num_results, backend=self.config.duckduckgo_backend
)
if search_results:
break

View File

@@ -55,7 +55,7 @@ class BrowsingError(CommandExecutionError):
class WebSeleniumConfiguration(BaseModel):
model_name: ModelName = OpenAIModelName.GPT3
llm_name: ModelName = OpenAIModelName.GPT3
"""Name of the llm model used to read websites"""
web_browser: Literal["chrome", "firefox", "safari", "edge"] = "chrome"
"""Web browser used by Selenium"""
@@ -68,6 +68,8 @@ class WebSeleniumConfiguration(BaseModel):
"""User agent used by the browser"""
browse_spacy_language_model: str = "en_core_web_sm"
"""Spacy language model used for chunking text"""
selenium_proxy: Optional[str] = None
"""Http proxy to use with Selenium"""
class WebSeleniumComponent(
@@ -164,7 +166,7 @@ class WebSeleniumComponent(
elif get_raw_content:
if (
output_tokens := self.llm_provider.count_tokens(
text, self.config.model_name
text, self.config.llm_name
)
) > MAX_RAW_CONTENT_LENGTH:
oversize_factor = round(output_tokens / MAX_RAW_CONTENT_LENGTH, 1)
@@ -301,6 +303,9 @@ class WebSeleniumComponent(
options.add_argument("--headless=new")
options.add_argument("--disable-gpu")
if self.config.selenium_proxy:
options.add_argument(f"--proxy-server={self.config.selenium_proxy}")
self._sideload_chrome_extensions(options, self.data_dir / "assets" / "crx")
if (chromium_driver_path := Path("/usr/bin/chromedriver")).exists():
@@ -382,7 +387,7 @@ class WebSeleniumComponent(
text,
topics_of_interest=topics_of_interest,
llm_provider=self.llm_provider,
model_name=self.config.model_name,
model_name=self.config.llm_name,
spacy_model=self.config.browse_spacy_language_model,
)
return "\n".join(f"* {i}" for i in information)
@@ -391,7 +396,7 @@ class WebSeleniumComponent(
text,
question=question,
llm_provider=self.llm_provider,
model_name=self.config.model_name,
model_name=self.config.llm_name,
spacy_model=self.config.browse_spacy_language_model,
)
return result

View File

@@ -10,7 +10,7 @@ from .schema import ChatPrompt, LanguageModelClassification
class PromptStrategy(abc.ABC):
@property
@abc.abstractmethod
def model_classification(self) -> LanguageModelClassification:
def llm_classification(self) -> LanguageModelClassification:
...
@abc.abstractmethod

View File

@@ -224,7 +224,7 @@ class BaseOpenAIChatProvider(
tool_calls=tool_calls or None,
),
parsed_result=parsed_result,
model_info=self.CHAT_MODELS[model_name],
llm_info=self.CHAT_MODELS[model_name],
prompt_tokens_used=t_input,
completion_tokens_used=t_output,
)
@@ -457,7 +457,7 @@ class BaseOpenAIEmbeddingProvider(
return EmbeddingModelResponse(
embedding=embedding_parser(response.data[0].embedding),
model_info=self.EMBEDDING_MODELS[model_name],
llm_info=self.EMBEDDING_MODELS[model_name],
prompt_tokens_used=response.usage.prompt_tokens,
)

View File

@@ -309,7 +309,7 @@ class AnthropicProvider(BaseChatModelProvider[AnthropicModelName, AnthropicSetti
return ChatModelResponse(
response=assistant_msg,
parsed_result=parsed_result,
model_info=ANTHROPIC_CHAT_MODELS[model_name],
llm_info=ANTHROPIC_CHAT_MODELS[model_name],
prompt_tokens_used=t_input,
completion_tokens_used=t_output,
)

View File

@@ -0,0 +1,36 @@
# Llamafile Integration Notes
Tested with:
* Python 3.11
* Apple M2 Pro (32 GB), macOS 14.2.1
* quantized mistral-7b-instruct-v0.2
## Setup
Download a `mistral-7b-instruct-v0.2` llamafile:
```shell
wget -nc https://huggingface.co/jartine/Mistral-7B-Instruct-v0.2-llamafile/resolve/main/mistral-7b-instruct-v0.2.Q5_K_M.llamafile
chmod +x mistral-7b-instruct-v0.2.Q5_K_M.llamafile
./mistral-7b-instruct-v0.2.Q5_K_M.llamafile --version
```
Run the llamafile server:
```shell
LLAMAFILE="./mistral-7b-instruct-v0.2.Q5_K_M.llamafile"
"${LLAMAFILE}" \
--server \
--nobrowser \
--ctx-size 0 \
--n-predict 1024
# note: ctx-size=0 means the prompt context size will be set directly from the
# underlying model configuration. This may cause slow response times or consume
# a lot of memory.
```
## TODOs
* `SMART_LLM`/`FAST_LLM` configuration: Currently, the llamafile server only serves one model at a time. However, there's no reason you can't start multiple llamafile servers on different ports. To support using different models for `smart_llm` and `fast_llm`, you could implement config vars like `LLAMAFILE_SMART_LLM_URL` and `LLAMAFILE_FAST_LLM_URL` that point to different llamafile servers (one serving a 'big model' and one serving a 'fast model').
* Authorization: the `serve.sh` script does not set up any authorization for the llamafile server; this can be turned on by adding arg `--api-key <some-key>` to the server startup command. However I haven't attempted to test whether the integration with autogpt works when this feature is turned on.
* Test with other models

View File

@@ -0,0 +1,17 @@
from .llamafile import (
LLAMAFILE_CHAT_MODELS,
LLAMAFILE_EMBEDDING_MODELS,
LlamafileCredentials,
LlamafileModelName,
LlamafileProvider,
LlamafileSettings,
)
__all__ = [
"LLAMAFILE_CHAT_MODELS",
"LLAMAFILE_EMBEDDING_MODELS",
"LlamafileCredentials",
"LlamafileModelName",
"LlamafileProvider",
"LlamafileSettings",
]

View File

@@ -0,0 +1,351 @@
import enum
import logging
import re
from pathlib import Path
from typing import Any, Iterator, Optional, Sequence
import requests
from openai.types.chat import (
ChatCompletionMessage,
ChatCompletionMessageParam,
CompletionCreateParams,
)
from pydantic import SecretStr
from forge.json.parsing import json_loads
from forge.models.config import UserConfigurable
from .._openai_base import BaseOpenAIChatProvider
from ..schema import (
AssistantToolCall,
AssistantToolCallDict,
ChatMessage,
ChatModelInfo,
CompletionModelFunction,
ModelProviderConfiguration,
ModelProviderCredentials,
ModelProviderName,
ModelProviderSettings,
ModelTokenizer,
)
class LlamafileModelName(str, enum.Enum):
MISTRAL_7B_INSTRUCT = "mistral-7b-instruct-v0.2"
LLAMAFILE_CHAT_MODELS = {
info.name: info
for info in [
ChatModelInfo(
name=LlamafileModelName.MISTRAL_7B_INSTRUCT,
provider_name=ModelProviderName.LLAMAFILE,
prompt_token_cost=0.0,
completion_token_cost=0.0,
max_tokens=32768,
has_function_call_api=False,
),
]
}
LLAMAFILE_EMBEDDING_MODELS = {}
class LlamafileConfiguration(ModelProviderConfiguration):
# TODO: implement 'seed' across forge.llm.providers
seed: Optional[int] = None
class LlamafileCredentials(ModelProviderCredentials):
api_key: Optional[SecretStr] = SecretStr("sk-no-key-required")
api_base: SecretStr = UserConfigurable( # type: ignore
default=SecretStr("http://localhost:8080/v1"), from_env="LLAMAFILE_API_BASE"
)
def get_api_access_kwargs(self) -> dict[str, str]:
return {
k: v.get_secret_value()
for k, v in {
"api_key": self.api_key,
"base_url": self.api_base,
}.items()
if v is not None
}
class LlamafileSettings(ModelProviderSettings):
configuration: LlamafileConfiguration # type: ignore
credentials: Optional[LlamafileCredentials] = None # type: ignore
class LlamafileTokenizer(ModelTokenizer[int]):
def __init__(self, credentials: LlamafileCredentials):
self._credentials = credentials
@property
def _tokenizer_base_url(self):
# The OpenAI-chat-compatible base url should look something like
# 'http://localhost:8080/v1' but the tokenizer endpoint is
# 'http://localhost:8080/tokenize'. So here we just strip off the '/v1'.
api_base = self._credentials.api_base.get_secret_value()
return api_base.strip("/v1")
def encode(self, text: str) -> list[int]:
response = requests.post(
url=f"{self._tokenizer_base_url}/tokenize", json={"content": text}
)
response.raise_for_status()
return response.json()["tokens"]
def decode(self, tokens: list[int]) -> str:
response = requests.post(
url=f"{self._tokenizer_base_url}/detokenize", json={"tokens": tokens}
)
response.raise_for_status()
return response.json()["content"]
class LlamafileProvider(
BaseOpenAIChatProvider[LlamafileModelName, LlamafileSettings],
# TODO: add and test support for embedding models
# BaseOpenAIEmbeddingProvider[LlamafileModelName, LlamafileSettings],
):
EMBEDDING_MODELS = LLAMAFILE_EMBEDDING_MODELS
CHAT_MODELS = LLAMAFILE_CHAT_MODELS
MODELS = {**CHAT_MODELS, **EMBEDDING_MODELS}
default_settings = LlamafileSettings(
name="llamafile_provider",
description=(
"Provides chat completion and embedding services "
"through a llamafile instance"
),
configuration=LlamafileConfiguration(),
)
_settings: LlamafileSettings
_credentials: LlamafileCredentials
_configuration: LlamafileConfiguration
async def get_available_models(self) -> Sequence[ChatModelInfo[LlamafileModelName]]:
_models = (await self._client.models.list()).data
# note: at the moment, llamafile only serves one model at a time (so this
# list will only ever have one value). however, in the future, llamafile
# may support multiple models, so leaving this method as-is for now.
self._logger.debug(f"Retrieved llamafile models: {_models}")
clean_model_ids = [clean_model_name(m.id) for m in _models]
self._logger.debug(f"Cleaned llamafile model IDs: {clean_model_ids}")
return [
LLAMAFILE_CHAT_MODELS[id]
for id in clean_model_ids
if id in LLAMAFILE_CHAT_MODELS
]
def get_tokenizer(self, model_name: LlamafileModelName) -> LlamafileTokenizer:
return LlamafileTokenizer(self._credentials)
def count_message_tokens(
self,
messages: ChatMessage | list[ChatMessage],
model_name: LlamafileModelName,
) -> int:
if isinstance(messages, ChatMessage):
messages = [messages]
if model_name == LlamafileModelName.MISTRAL_7B_INSTRUCT:
# For mistral-instruct, num added tokens depends on if the message
# is a prompt/instruction or an assistant-generated message.
# - prompt gets [INST], [/INST] added and the first instruction
# begins with '<s>' ('beginning-of-sentence' token).
# - assistant-generated messages get '</s>' added
# see: https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.2
#
prompt_added = 1 # one for '<s>' token
assistant_num_added = 0
ntokens = 0
for message in messages:
if (
message.role == ChatMessage.Role.USER
# note that 'system' messages will get converted
# to 'user' messages before being sent to the model
or message.role == ChatMessage.Role.SYSTEM
):
# 5 tokens for [INST], [/INST], which actually get
# tokenized into "[, INST, ]" and "[, /, INST, ]"
# by the mistral tokenizer
prompt_added += 5
elif message.role == ChatMessage.Role.ASSISTANT:
assistant_num_added += 1 # for </s>
else:
raise ValueError(
f"{model_name} does not support role: {message.role}"
)
ntokens += self.count_tokens(message.content, model_name)
total_token_count = prompt_added + assistant_num_added + ntokens
return total_token_count
else:
raise NotImplementedError(
f"count_message_tokens not implemented for model {model_name}"
)
def _get_chat_completion_args(
self,
prompt_messages: list[ChatMessage],
model: LlamafileModelName,
functions: list[CompletionModelFunction] | None = None,
max_output_tokens: int | None = None,
**kwargs,
) -> tuple[
list[ChatCompletionMessageParam], CompletionCreateParams, dict[str, Any]
]:
messages, completion_kwargs, parse_kwargs = super()._get_chat_completion_args(
prompt_messages, model, functions, max_output_tokens, **kwargs
)
if model == LlamafileModelName.MISTRAL_7B_INSTRUCT:
messages = self._adapt_chat_messages_for_mistral_instruct(messages)
if "seed" not in kwargs and self._configuration.seed is not None:
completion_kwargs["seed"] = self._configuration.seed
# Convert all messages with content blocks to simple text messages
for message in messages:
if isinstance(content := message.get("content"), list):
message["content"] = "\n\n".join(
b["text"]
for b in content
if b["type"] == "text"
# FIXME: add support for images through image_data completion kwarg
)
return messages, completion_kwargs, parse_kwargs
def _adapt_chat_messages_for_mistral_instruct(
self, messages: list[ChatCompletionMessageParam]
) -> list[ChatCompletionMessageParam]:
"""
Munge the messages to be compatible with the mistral-7b-instruct chat
template, which:
- only supports 'user' and 'assistant' roles.
- expects messages to alternate between user/assistant roles.
See details here:
https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.2#instruction-format
"""
adapted_messages: list[ChatCompletionMessageParam] = []
for message in messages:
# convert 'system' role to 'user' role as mistral-7b-instruct does
# not support 'system'
if message["role"] == ChatMessage.Role.SYSTEM:
message["role"] = ChatMessage.Role.USER
if (
len(adapted_messages) == 0
or message["role"] != (last_message := adapted_messages[-1])["role"]
):
adapted_messages.append(message)
else:
if not message.get("content"):
continue
# if the curr message has the same role as the previous one,
# concat the current message content to the prev message
if message["role"] == "user" and last_message["role"] == "user":
# user messages can contain other types of content blocks
if not isinstance(last_message["content"], list):
last_message["content"] = [
{"type": "text", "text": last_message["content"]}
]
last_message["content"].extend(
message["content"]
if isinstance(message["content"], list)
else [{"type": "text", "text": message["content"]}]
)
elif message["role"] != "user" and last_message["role"] != "user":
last_message["content"] = (
(last_message.get("content") or "")
+ "\n\n"
+ (message.get("content") or "")
).strip()
return adapted_messages
def _parse_assistant_tool_calls(
self,
assistant_message: ChatCompletionMessage,
compat_mode: bool = False,
**kwargs,
):
tool_calls: list[AssistantToolCall] = []
parse_errors: list[Exception] = []
if compat_mode and assistant_message.content:
try:
tool_calls = list(
_tool_calls_compat_extract_calls(assistant_message.content)
)
except Exception as e:
parse_errors.append(e)
return tool_calls, parse_errors
def clean_model_name(model_file: str) -> str:
"""
Clean up model names:
1. Remove file extension
2. Remove quantization info
Examples:
```
raw: 'mistral-7b-instruct-v0.2.Q5_K_M.gguf'
clean: 'mistral-7b-instruct-v0.2'
raw: '/Users/kate/models/mistral-7b-instruct-v0.2.Q5_K_M.gguf'
clean: 'mistral-7b-instruct-v0.2'
raw: 'llava-v1.5-7b-q4.gguf'
clean: 'llava-v1.5-7b'
```
"""
name_without_ext = Path(model_file).name.rsplit(".", 1)[0]
name_without_Q = re.match(
r"^[a-zA-Z0-9]+([.\-](?!([qQ]|B?F)\d{1,2})[a-zA-Z0-9]+)*",
name_without_ext,
)
return name_without_Q.group() if name_without_Q else name_without_ext
def _tool_calls_compat_extract_calls(response: str) -> Iterator[AssistantToolCall]:
import re
import uuid
logging.debug(f"Trying to extract tool calls from response:\n{response}")
response = response.strip() # strip off any leading/trailing whitespace
if response.startswith("```"):
# attempt to remove any extraneous markdown artifacts like "```json"
response = response.strip("```")
if response.startswith("json"):
response = response.strip("json")
response = response.strip() # any remaining whitespace
if response[0] == "[":
tool_calls: list[AssistantToolCallDict] = json_loads(response)
else:
block = re.search(r"```(?:tool_calls)?\n(.*)\n```\s*$", response, re.DOTALL)
if not block:
raise ValueError("Could not find tool_calls block in response")
tool_calls: list[AssistantToolCallDict] = json_loads(block.group(1))
for t in tool_calls:
t["id"] = str(uuid.uuid4())
# t["function"]["arguments"] = str(t["function"]["arguments"]) # HACK
yield AssistantToolCall.parse_obj(t)

View File

@@ -1,12 +1,13 @@
from __future__ import annotations
import logging
from typing import Any, Callable, Iterator, Optional, Sequence, TypeVar, get_args
from typing import Any, AsyncIterator, Callable, Optional, Sequence, TypeVar, get_args
from pydantic import ValidationError
from .anthropic import ANTHROPIC_CHAT_MODELS, AnthropicModelName, AnthropicProvider
from .groq import GROQ_CHAT_MODELS, GroqModelName, GroqProvider
from .llamafile import LLAMAFILE_CHAT_MODELS, LlamafileModelName, LlamafileProvider
from .openai import OPEN_AI_CHAT_MODELS, OpenAIModelName, OpenAIProvider
from .schema import (
AssistantChatMessage,
@@ -24,10 +25,15 @@ from .schema import (
_T = TypeVar("_T")
ModelName = AnthropicModelName | GroqModelName | OpenAIModelName
ModelName = AnthropicModelName | GroqModelName | LlamafileModelName | OpenAIModelName
EmbeddingModelProvider = OpenAIProvider
CHAT_MODELS = {**ANTHROPIC_CHAT_MODELS, **GROQ_CHAT_MODELS, **OPEN_AI_CHAT_MODELS}
CHAT_MODELS = {
**ANTHROPIC_CHAT_MODELS,
**GROQ_CHAT_MODELS,
**LLAMAFILE_CHAT_MODELS,
**OPEN_AI_CHAT_MODELS,
}
class MultiProvider(BaseChatModelProvider[ModelName, ModelProviderSettings]):
@@ -62,7 +68,7 @@ class MultiProvider(BaseChatModelProvider[ModelName, ModelProviderSettings]):
async def get_available_chat_models(self) -> Sequence[ChatModelInfo[ModelName]]:
models = []
for provider in self.get_available_providers():
async for provider in self.get_available_providers():
models.extend(await provider.get_available_chat_models())
return models
@@ -114,37 +120,58 @@ class MultiProvider(BaseChatModelProvider[ModelName, ModelProviderSettings]):
model_info = CHAT_MODELS[model]
return self._get_provider(model_info.provider_name)
def get_available_providers(self) -> Iterator[ChatModelProvider]:
async def get_available_providers(self) -> AsyncIterator[ChatModelProvider]:
for provider_name in ModelProviderName:
self._logger.debug(f"Checking if provider {provider_name} is available...")
try:
yield self._get_provider(provider_name)
except Exception:
provider = self._get_provider(provider_name)
await provider.get_available_models() # check connection
yield provider
self._logger.debug(f"Provider '{provider_name}' is available!")
except ValueError:
pass
except Exception as e:
self._logger.debug(f"Provider '{provider_name}' is failing: {e}")
def _get_provider(self, provider_name: ModelProviderName) -> ChatModelProvider:
_provider = self._provider_instances.get(provider_name)
if not _provider:
Provider = self._get_provider_class(provider_name)
self._logger.debug(
f"{Provider.__name__} not yet in cache, trying to init..."
)
settings = Provider.default_settings.model_copy(deep=True)
settings.budget = self._budget
settings.configuration.extra_request_headers.update(
self._settings.configuration.extra_request_headers
)
if settings.credentials is None:
credentials_field = settings.model_fields["credentials"]
Credentials = get_args( # Union[Credentials, None] -> Credentials
credentials_field.annotation
)[0]
self._logger.debug(f"Loading {Credentials.__name__}...")
try:
Credentials = get_args( # Union[Credentials, None] -> Credentials
settings.model_fields["credentials"].annotation
)[0]
settings.credentials = Credentials.from_env()
except ValidationError as e:
raise ValueError(
f"{provider_name} is unavailable: can't load credentials"
) from e
if credentials_field.is_required():
self._logger.debug(
f"Could not load (required) {Credentials.__name__}"
)
raise ValueError(
f"{Provider.__name__} is unavailable: "
"can't load credentials"
) from e
self._logger.debug(
f"Could not load {Credentials.__name__}, continuing without..."
)
self._provider_instances[provider_name] = _provider = Provider(
settings=settings, logger=self._logger # type: ignore
)
_provider._budget = self._budget # Object binding not preserved by Pydantic
self._logger.debug(f"Initialized {Provider.__name__}!")
return _provider
@classmethod
@@ -155,6 +182,7 @@ class MultiProvider(BaseChatModelProvider[ModelName, ModelProviderSettings]):
return {
ModelProviderName.ANTHROPIC: AnthropicProvider,
ModelProviderName.GROQ: GroqProvider,
ModelProviderName.LLAMAFILE: LlamafileProvider,
ModelProviderName.OPENAI: OpenAIProvider,
}[provider_name]
except KeyError:
@@ -164,4 +192,10 @@ class MultiProvider(BaseChatModelProvider[ModelName, ModelProviderSettings]):
return f"{self.__class__.__name__}()"
ChatModelProvider = AnthropicProvider | GroqProvider | OpenAIProvider | MultiProvider
ChatModelProvider = (
AnthropicProvider
| GroqProvider
| LlamafileProvider
| OpenAIProvider
| MultiProvider
)

View File

@@ -55,6 +55,7 @@ class ModelProviderName(str, enum.Enum):
OPENAI = "openai"
ANTHROPIC = "anthropic"
GROQ = "groq"
LLAMAFILE = "llamafile"
class ChatMessage(BaseModel):
@@ -186,7 +187,7 @@ class ModelResponse(BaseModel):
prompt_tokens_used: int
completion_tokens_used: int
model_info: ModelInfo
llm_info: ModelInfo
class ModelProviderConfiguration(SystemConfiguration):

69
forge/poetry.lock generated
View File

@@ -5642,41 +5642,41 @@ files = [
[[package]]
name = "spacy"
version = "3.7.4"
version = "3.7.5"
description = "Industrial-strength Natural Language Processing (NLP) in Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "spacy-3.7.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0f748625192f573c07ddea5fcd324919dbfbf4f4a2f7a1fc731e6dcba7321ea1"},
{file = "spacy-3.7.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6288dca7b3a5489b3d7ce68404bc432ca22f826c662a12af47ef7bdb264307fb"},
{file = "spacy-3.7.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef59db99b12a72d2646be3888d87f94c59e11cd07adc2f50a8130e83f07eb1cf"},
{file = "spacy-3.7.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f07477a4027711c22b3865e78dc9076335c03fcf318a6736159bf07e2a923125"},
{file = "spacy-3.7.4-cp310-cp310-win_amd64.whl", hash = "sha256:787ce42a837f7edfbd4185356eea893a81b7dd75743d0047f2b9bf179775f970"},
{file = "spacy-3.7.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e82b9da21853d4aee46811804dc7e136895f087fda25c7585172d95eb9b70833"},
{file = "spacy-3.7.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07ffedf51899441070fb70432f8f873696f39e0e31c9ce7403101c459f8a1281"},
{file = "spacy-3.7.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba57bcc111eca7b086ee33a9636df775cfd4b14302f7d0ffbc11e95ac0fb3f0e"},
{file = "spacy-3.7.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7580d1565f4d1ccbee9a18531f993a5b9b37ced96f145153dd4e98ceec607a55"},
{file = "spacy-3.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:df99c6f0085b1ec8e88beb5fd96d4371cef6fc19c202c41fc4fadc2afd55a157"},
{file = "spacy-3.7.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b982ebab417189346acb4722637c573830d62e157ba336c3eb6c417249344be1"},
{file = "spacy-3.7.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e7c29e152d8ea060af60da9410fa8ef038f3c9068a206905ee5c704de78f6e87"},
{file = "spacy-3.7.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:023c9a008328f55c4717c56c4f8a28073b9961547f7d38a9405c967a52e66d59"},
{file = "spacy-3.7.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1969d3d0fd0c811b7485438460f0ae8cfe16d46b54bcb8d1c26e70914e67e3d"},
{file = "spacy-3.7.4-cp312-cp312-win_amd64.whl", hash = "sha256:040f7df5096c817450820eaaa426d54ed266254d16974e9a707a32f5b0f139ae"},
{file = "spacy-3.7.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6757e8fbfd35dc0ed830296d5756f46d5b8d4b0353925dbe2f9aa33b82c5308"},
{file = "spacy-3.7.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c500c1bad9e0488814a75077089aeef64a6b520ae8131578f266a08168106fa3"},
{file = "spacy-3.7.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c992e2c5c0cd06c7f3e74fe8d758885117090013931c7938277d1421660bf71f"},
{file = "spacy-3.7.4-cp37-cp37m-win_amd64.whl", hash = "sha256:2463c56ab1378f2b9a675340a2e3dfb618989d0da8cdce06429bc9b1dad4f294"},
{file = "spacy-3.7.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b43e92edfa99f34dbb9dd30175f41158d20945e3179055d0071fee19394add96"},
{file = "spacy-3.7.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c26a81d33c93e4a8e3360d61dcce0802fb886de79f666a487ea5abbd3ce4b30b"},
{file = "spacy-3.7.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d7910ca7a91bf423febd8a9a10ca6a4cfcb5c99abdec79df1eb7b67ea3e3c90"},
{file = "spacy-3.7.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b16768b9e5c350b8a383a6bd84cd0481ccdf10ae6231f568598890638065f69"},
{file = "spacy-3.7.4-cp38-cp38-win_amd64.whl", hash = "sha256:ed99fb176979b1e3cf6830161f8e881beae54e80147b05fca31d9a67cb12fbca"},
{file = "spacy-3.7.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ca8112330982dbeef125cc5eb40e0349493055835a0ebe29028a0953a25d8522"},
{file = "spacy-3.7.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:977f37493d7cf0b5dca155f0450d47890378703283c29919cdcc220db994a775"},
{file = "spacy-3.7.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ad5e931c294d100ec3edb40e40f2722ef505cea16312839dd6467e81d665740"},
{file = "spacy-3.7.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11ebf6054cd3ec3638801d7ff9b709e32fb9c15512b347b489bfe2ccb1102c9f"},
{file = "spacy-3.7.4-cp39-cp39-win_amd64.whl", hash = "sha256:f5b930753027ac599f70bb7e77d6a2256191fe582e6f3f0cd624d88f6c279fa4"},
{file = "spacy-3.7.4.tar.gz", hash = "sha256:525f2ced2e40761562c8cace93ef6a1e6e8c483f27bd564bc1b15f608efbe85b"},
{file = "spacy-3.7.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8002897701429ee2ab5ff6921ae43560f4cd17184cb1e10dad761901c12dcb85"},
{file = "spacy-3.7.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:43acd19efc845e9126b61a05ed7508a0aff509e96e15563f30f810c19e636b7c"},
{file = "spacy-3.7.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f044522b1271ea54718dc43b6f593b5dad349cd31b3827764c501529b599e09a"},
{file = "spacy-3.7.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a7dbfbca42c1c128fefa6832631fe49e11c850e963af99229f14e2d0ae94f34"},
{file = "spacy-3.7.5-cp310-cp310-win_amd64.whl", hash = "sha256:2a21b2a1e1e5d10d15c6f75990b7341d0fc9b454083dfd4222fdd75b9164831c"},
{file = "spacy-3.7.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cd93c34bf2a02bbed7df73d42aed8df5e3eb9688c4ea84ec576f740ba939cce5"},
{file = "spacy-3.7.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:190ba0032a5efdb138487c587c0ebb7a98f86adb917f464b252ee8766b8eec4a"},
{file = "spacy-3.7.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38de1c9bbb73b8cdfea2dd6e57450f093c1a1af47515870c1c8640b85b35ab16"},
{file = "spacy-3.7.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dad4853950a2fe6c7a0bdfd791a762d1f8cedd2915c4ae41b2e0ca3a850eefc"},
{file = "spacy-3.7.5-cp311-cp311-win_amd64.whl", hash = "sha256:4e00d076871af784c2e43185a71ee676b58893853a05c5b81717b8af2b666c07"},
{file = "spacy-3.7.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bf54c3c2425428b328b53a65913d47eb4cb27a1429aa4e8ed979ffc97d4663e0"},
{file = "spacy-3.7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4145cea7f9814fa7d86b2028c2dd83e02f13f80d5ac604a400b2f7d7b26a0e8c"},
{file = "spacy-3.7.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:262f8ebb71f7ed5ffe8e4f384b2594b7a296be50241ce9fbd9277b5da2f46f38"},
{file = "spacy-3.7.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:faa1e2b6234ae33c0b1f8dfa5a8dcb66fb891f19231725dfcff4b2666125c250"},
{file = "spacy-3.7.5-cp312-cp312-win_amd64.whl", hash = "sha256:07677e270a6d729453cc04b5e2247a96a86320b8845e6428d9f90f217eff0f56"},
{file = "spacy-3.7.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e207dda0639818e2ef8f12e3df82a526de118cc09082b0eee3053ebcd9f8332"},
{file = "spacy-3.7.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5694dd3b2f6414c18e2a3f31111cd41ffd597e1d614b51c5779f85ff07f08f6c"},
{file = "spacy-3.7.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d211920ff73d68b8febb1d293f10accbd54f2b2228ecd3530548227b750252b1"},
{file = "spacy-3.7.5-cp37-cp37m-win_amd64.whl", hash = "sha256:1171bf4d8541c18a83441be01feb6c735ffc02e9308810cd691c8900a6678cd5"},
{file = "spacy-3.7.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d9108f67675fb2078ed77cda61fd4cfc197f9256c28d35cfd946dcb080190ddc"},
{file = "spacy-3.7.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:12fdc01a4391299a47f16915505cc515fd059e71c7239904e216523354eeb9d9"},
{file = "spacy-3.7.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f8fbe9f6b9de1bf05d163a9dd88108b8f20b138986e6ed36f960832e3fcab33"},
{file = "spacy-3.7.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d244d524ab5a33530ac5c50fc92c9a41da6c3980f452048b9fc29e1ff1bdd03e"},
{file = "spacy-3.7.5-cp38-cp38-win_amd64.whl", hash = "sha256:8b493a8b79a7f3754102fa5ef7e2615568a390fec7ea20db49af55e5f0841fcf"},
{file = "spacy-3.7.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fdbb667792d6ca93899645774d1db3fccc327088a92072029be1e4bc25d7cf15"},
{file = "spacy-3.7.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4cfb85309e11a39681c9d4941aebb95c1f5e2e3b77a61a5451e2c3849da4b92e"},
{file = "spacy-3.7.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b0bf1788ca397eef8e67e9c07cfd9287adac438512dd191e6e6ca0f36357201"},
{file = "spacy-3.7.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:591d90d8504e9bd5be5b482be7c6d6a974afbaeb62c3181e966f4e407e0ab300"},
{file = "spacy-3.7.5-cp39-cp39-win_amd64.whl", hash = "sha256:713b56fe008c79df01617f3602a0b7e523292211337eb999bdffb910ea1f4825"},
{file = "spacy-3.7.5.tar.gz", hash = "sha256:a648c6cbf2acc7a55a69ee9e7fa4f22bdf69aa828a587a1bc5cfff08cf3c2dd3"},
]
[package.dependencies]
@@ -5691,15 +5691,14 @@ preshed = ">=3.0.2,<3.1.0"
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0"
requests = ">=2.13.0,<3.0.0"
setuptools = "*"
smart-open = ">=5.2.1,<7.0.0"
spacy-legacy = ">=3.0.11,<3.1.0"
spacy-loggers = ">=1.0.0,<2.0.0"
srsly = ">=2.4.3,<3.0.0"
thinc = ">=8.2.2,<8.3.0"
tqdm = ">=4.38.0,<5.0.0"
typer = ">=0.3.0,<0.10.0"
typer = ">=0.3.0,<1.0.0"
wasabi = ">=0.9.1,<1.2.0"
weasel = ">=0.1.0,<0.4.0"
weasel = ">=0.1.0,<0.5.0"
[package.extras]
apple = ["thinc-apple-ops (>=0.1.0.dev0,<1.0.0)"]
@@ -7085,4 +7084,4 @@ benchmark = ["agbenchmark"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "7523abd672967cbe924f045a00bf519ee08c8537fdf2f2191d2928201497d7b7"
content-hash = "acca6b5d67a64527f1d19f61e20a89eb228e066a80cd7701fd59cf19bb267eb8"

View File

@@ -33,6 +33,7 @@ gTTS = "^2.3.1"
jinja2 = "^3.1.2"
jsonschema = "*"
litellm = "^1.17.9"
numpy = ">=1.26.0,<2.0.0"
openai = "^1.7.2"
Pillow = "*"
playsound = "~1.2.2"
@@ -51,7 +52,7 @@ spacy = "^3.0.0"
tenacity = "^8.2.2"
tiktoken = ">=0.7.0,<1.0.0"
toml = "^0.10.2"
uvicorn = ">=0.23.2,<1"
uvicorn = { extras = ["standard"], version = ">=0.23.2,<1" }
watchdog = "4.0.0"
webdriver-manager = "^4.0.1"

View File

@@ -1,6 +1,6 @@
## [AutoGPT Forge Part 1: A Comprehensive Guide to Your First Steps](https://aiedge.medium.com/autogpt-forge-a-comprehensive-guide-to-your-first-steps-a1dfdf46e3b4)
![Header](../../../docs/content/imgs/quickstart/000_header_img.png)
![Header](..%2F..%2F..%2Fdocs/content/imgs/quickstart/000_header_img.png)
**Written by Craig Swift & [Ryan Brandt](https://github.com/paperMoose)**
@@ -15,22 +15,22 @@ The Forge serves as a comprehensive template for building your own AutoGPT agent
To begin, you need to fork the [repository](https://github.com/Significant-Gravitas/AutoGPT) by navigating to the main page of the repository and clicking **Fork** in the top-right corner.
![The Github repository](../../../docs/content/imgs/quickstart/001_repo.png)
![The Github repository](..%2F..%2F..%2Fdocs/content/imgs/quickstart/001_repo.png)
Follow the on-screen instructions to complete the process.
![Create Fork Page](../../../docs/content/imgs/quickstart/002_fork.png)
![Create Fork Page](..%2F..%2F..%2Fdocs/content/imgs/quickstart/002_fork.png)
### Cloning the Repository
Next, clone your newly forked repository to your local system. Ensure you have Git installed to proceed with this step. You can download Git from [here](https://git-scm.com/downloads). Then clone the repo using the following command and the url for your repo. You can find the correct url by clicking on the green Code button on your repos main page.
![img_1.png](../../../docs/content/imgs/quickstart/003A_clone.png)
![img_1.png](..%2F..%2F..%2Fdocs/content/imgs/quickstart/003A_clone.png)
```bash
# replace the url with the one for your forked repo
git clone https://github.com/<YOUR REPO PATH HERE>
```
![Clone the Repository](../../../docs/content/imgs/quickstart/003_clone.png)
![Clone the Repository](..%2F..%2F..%2Fdocs/content/imgs/quickstart/003_clone.png)
### Setting up the Project
@@ -41,8 +41,8 @@ cd AutoGPT
```
To set up the project, utilize the `./run setup` command in the terminal. Follow the instructions to install necessary dependencies and set up your GitHub access token.
![Setup the Project](../../../docs/content/imgs/quickstart/005_setup.png)
![Setup Complete](../../../docs/content/imgs/quickstart/006_setup_complete.png)
![Setup the Project](..%2F..%2F..%2Fdocs/content/imgs/quickstart/005_setup.png)
![Setup Complete](..%2F..%2F..%2Fdocs/content/imgs/quickstart/006_setup_complete.png)
## Section 3: Creating Your Agent
@@ -55,7 +55,7 @@ Create your agent template using the command:
```
Replacing YOUR_AGENT_NAME with the name you chose in the previous step.
![Create an Agent](../../../docs/content/imgs/quickstart/007_create_agent.png)
![Create an Agent](..%2F..%2F..%2Fdocs/content/imgs/quickstart/007_create_agent.png)
## Section 4: Running Your Agent
@@ -66,13 +66,13 @@ Begin by starting your agent using the command:
```
This will initiate the agent on `http://localhost:8000/`.
![Start the Agent](../../../docs/content/imgs/quickstart/009_start_agent.png)
![Start the Agent](..%2F..%2F..%2Fdocs/content/imgs/quickstart/009_start_agent.png)
### Logging in and Sending Tasks to Your Agent
Access the frontend at `http://localhost:8000/` and log in using a Google or GitHub account. Once you're logged you'll see the agent tasking interface! However... the agent won't do anything yet. We'll implement the logic for our agent to run tasks in the upcoming tutorial chapters.
![Login](../../../docs/content/imgs/quickstart/010_login.png)
![Home](../../../docs/content/imgs/quickstart/011_home.png)
![Login](..%2F..%2F..%2Fdocs/content/imgs/quickstart/010_login.png)
![Home](..%2F..%2F..%2Fdocs/content/imgs/quickstart/011_home.png)
### Stopping and Restarting Your Agent
When needed, use Ctrl+C to end the session or use the stop command:

View File

@@ -7,7 +7,7 @@
---
![Header](../../../docs/content/imgs/quickstart/t2_01.png)
![Header](..%2F..%2Fdocs/content/imgs/quickstart/t2_01.png)
@@ -21,14 +21,14 @@ Large Language Models (LLMs) are state-of-the-art machine learning models that h
Traditional autonomous agents operated with limited knowledge, often confined to specific tasks or environments. They were like calculators — efficient but limited to predefined functions. LLM-based agents, on the other hand dont just compute; they understand, reason, and then act, drawing from a vast reservoir of information.
![AI visualising AI researchers hard at work](../../../docs/content/imgs/quickstart/t2_02.png)
![AI visualising AI researchers hard at work](..%2F..%2Fdocs/content/imgs/quickstart/t2_02.png)
## The Anatomy of an LLM-Based AI Agent
Diving deep into the core of an LLM-based AI agent, we find its structured much like a human, with distinct components akin to personality, memory, thought process, and abilities. Lets break these down:
![The Github repository](../../../docs/content/imgs/quickstart/t2_03.png)
![The Github repository](..%2F..%2Fdocs/content/imgs/quickstart/t2_03.png)
Anatomy of an Agent from the Agent Landscape Survey
### **Profile**

View File

@@ -1,6 +1,6 @@
# AutoGPT Forge: Crafting Intelligent Agent Logic
![Header](../../../docs/content/imgs/quickstart/t3_01.png)
![Header](..%2F..%2F..%2Fdocs/content/imgs/quickstart/t3_01.png)
**By Craig Swift & [Ryan Brandt](https://github.com/paperMoose)**
Hey there! Ready for part 3 of our AutoGPT Forge tutorial series? If you missed the earlier parts, catch up here:
@@ -17,7 +17,7 @@ Make sure you've set up your project and created an agent as described in our in
In the image below, you'll see my "SmartAgent" and the agent.py file inside the 'forge' folder. That's where we'll be adding our LLM-based logic. If you're unsure about the project structure or agent functions from our last guide, don't worry. We'll cover the basics as we go!
![SmartAgent](../../../docs/content/imgs/quickstart/t3_02.png)
![SmartAgent](..%2F..%2F..%2Fdocs/content/imgs/quickstart/t3_02.png)
---
@@ -125,7 +125,7 @@ Now that we've set this up, let's move to the next exciting part: The PromptEngi
**The Art of Prompting**
![Prompting 101](../../../docs/content/imgs/quickstart/t3_03.png)
![Prompting 101](..%2F..%2F..%2Fdocs/content/imgs/quickstart/t3_03.png)
Prompting is like shaping messages for powerful language models like ChatGPT. Since these models respond to input details, creating the right prompt can be a challenge. That's where the **PromptEngine** comes in.
@@ -479,7 +479,7 @@ d88P 888 "Y88888 "Y888 "Y88P" "Y8888P88 888 888
3. **Navigate to Benchmarking**
- Look to the left, and you'll spot a trophy icon. Click it to enter the benchmarking arena.
![Benchmarking page of the AutoGPT UI](../../../docs/content/imgs/quickstart/t3_04.png)
![Benchmarking page of the AutoGPT UI](..%2F..%2F..%2Fdocs/content/imgs/quickstart/t3_04.png)
4. **Select the 'WriteFile' Test**
- Choose the 'WriteFile' test from the available options.

36
rnd/README.md Normal file
View File

@@ -0,0 +1,36 @@
This is a guide to setting up and running the AutoGPT Server and Builder. This tutorial will cover downloading the necessary files, setting up the server, and testing the system.
https://github.com/user-attachments/assets/fd0d0f35-3155-4263-b575-ba3efb126cb4
1. Navigate to the AutoGPT GitHub repository.
2. Click the "Code" button, then select "Download ZIP".
3. Once downloaded, extract the ZIP file to a folder of your choice.
4. Open the extracted folder and navigate to the "rnd" directory.
5. Enter the "AutoGPT server" folder.
6. Open a terminal window in this directory.
7. Locate and open the README file in the AutoGPT server folder: [doc](./autogpt_server/README.md#setup).
8. Copy and paste each command from the setup section in the README into your terminal.
- Important: Wait for each command to finish before running the next one.
9. If all commands run without errors, enter the final command: `poetry run app`
10. You should now see the server running in your terminal.
11. Navigate back to the "rnd" folder.
12. Open the "AutoGPT builder" folder.
13. Open the README file in this folder: [doc](./autogpt_builder/README.md#getting-started).
14. In your terminal, run the following commands:
```
npm install
```
```
npm run dev
```
15. Once the front-end is running, click the link to navigate to `localhost:3000`.
16. Click on the "Build" option.
17. Add a few blocks to test the functionality.
18. Connect the blocks together.
19. Click "Run".
20. Check your terminal window - you should see that the server has received the request, is processing it, and has executed it.
And there you have it! You've successfully set up and tested AutoGPT.

0
rnd/__init__.py Normal file
View File

View File

@@ -1 +1 @@
AGPT_SERVER_URL=http://localhost:8000
AGPT_SERVER_URL=http://localhost:8000/api

30
rnd/autogpt_builder/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,30 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "yarn dev"
},
{
"name": "Next.js: debug client-side",
"type": "msedge",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Next.js: debug full stack",
"type": "node-terminal",
"request": "launch",
"command": "yarn dev",
"serverReadyAction": {
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"action": "debugWithEdge"
}
},
]
}

View File

@@ -0,0 +1,32 @@
# Base stage for both dev and prod
FROM node:21-alpine AS base
WORKDIR /app
COPY autogpt_builder/package.json autogpt_builder/yarn.lock ./
RUN yarn install --frozen-lockfile
# Dev stage
FROM base AS dev
ENV NODE_ENV=development
COPY autogpt_builder/ .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Build stage for prod
FROM base AS build
COPY autogpt_builder/ .
RUN npm run build
# Prod stage
FROM node:21-alpine AS prod
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build /app/package.json /app/yarn.lock ./
RUN yarn install --frozen-lockfile
COPY --from=build /app/.next ./.next
COPY --from=build /app/public ./public
COPY --from=build /app/next.config.js ./next.config.js
EXPOSE 3000
CMD ["npm", "start"]

View File

@@ -1,8 +0,0 @@
const dotenv = require('dotenv');
dotenv.config();
module.exports = {
env: {
AGPT_SERVER_URL: process.env.AGPT_SERVER_URL,
},
};

View File

@@ -1,5 +1,13 @@
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
/** @type {import('next').NextConfig} */
const nextConfig = {
env: {
AGPT_SERVER_URL: process.env.AGPT_SERVER_URL,
},
async redirects() {
return [
{
@@ -7,8 +15,8 @@ const nextConfig = {
destination: '/build',
permanent: false,
},
]
}
];
},
};
export default nextConfig;

View File

@@ -9,27 +9,36 @@
"lint": "next lint"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-avatar": "^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-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"ajv": "^8.17.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"moment": "^2.30.1",
"dotenv": "^16.4.5",
"lucide-react": "^0.407.0",
"moment": "^2.30.1",
"next": "14.2.4",
"next-themes": "^0.3.0",
"react": "^18",
"react-day-picker": "^8.10.1",
"react-dom": "^18",
"react-hook-form": "^7.52.1",
"react-markdown": "^9.0.1",
"react-modal": "^3.16.1",
"reactflow": "^11.11.4",
"recharts": "^2.12.7",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20",

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -1,72 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.8.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="AUTOgpt_logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 2000 2000" style="enable-background:new 0 0 2000 2000;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_00000044859330063917736280000017916509329539228544_);}
.st2{fill:url(#SVGID_00000140714777961496567230000017473346511890493859_);}
.st3{fill:url(#SVGID_00000016043459524955834950000015278934287808704695_);}
.st4{fill:url(#SVGID_00000133526441615091004900000013561443639704575621_);}
.st5{fill:#000030;}
.st6{fill:#669CF6;}
</style>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="17241.2793" y1="15058.8164" x2="17241.2793" y2="16623.8047" gradientTransform="matrix(7.200000e-02 0 0 7.200000e-02 0.928 1.072)">
<stop offset="0" style="stop-color:#000030"/>
<stop offset="1" style="stop-color:#9900FF"/>
</linearGradient>
<path class="st0" d="M1216.7,1078.8v86.8c0,6.4-5.2,11.6-11.6,11.6c-6.9,0-12.6-4.4-12.6-11.6V1036c0-27.5,22.3-49.8,49.8-49.8
s49.8,22.3,49.8,49.8c0,27.5-22.3,49.8-49.8,49.8C1233,1085.8,1224.2,1083.2,1216.7,1078.8L1216.7,1078.8z M1226.9,1020.6
c8.5,0,15.4,6.9,15.4,15.4s-6.9,15.4-15.4,15.4c-1.6,0-3.1-0.2-4.5-0.7c4.5,6.1,11.8,10.1,19.9,10.1c13.7,0,24.8-11.1,24.8-24.8
s-11.1-24.8-24.8-24.8c-8.2,0-15.4,4-19.9,10.1C1223.8,1020.9,1225.3,1020.6,1226.9,1020.6L1226.9,1020.6z"/>
<linearGradient id="SVGID_00000085938981603410528570000012380000869662973629_" gradientUnits="userSpaceOnUse" x1="15312.8066" y1="15057.3965" x2="15312.8066" y2="16624.1172" gradientTransform="matrix(7.200000e-02 0 0 7.200000e-02 0.928 1.072)">
<stop offset="0" style="stop-color:#000030"/>
<stop offset="1" style="stop-color:#4285F4"/>
</linearGradient>
<path style="fill:url(#SVGID_00000085938981603410528570000012380000869662973629_);" d="M1154.5,1078.8v55.8c0,5.1-2.1,9.7-5.4,13
c-7.3,7.3-20.9,7.3-28.2,0c-9.6-9.6-0.5-25.9-17.7-43.1c-16.7-16.7-45.8-16.7-62.5,0c-7.7,7.7-12.5,18.4-12.5,30.1
c0,6.4,5.2,11.6,11.6,11.6c6.9,0,12.6-4.4,12.6-11.6c0-5.1,2.1-9.7,5.4-13c7.3-7.3,20.9-7.3,28.2,0c10.5,10.5-0.1,25.3,17.7,43.1
c16.7,16.7,45.8,16.7,62.5,0c7.7-7.7,12.5-18.4,12.5-30.1v-98.2v-0.3c0-27.5-22.3-49.8-49.8-49.8c-27.5,0-49.8,22.3-49.8,49.8
c0,27.5,22.3,49.8,49.8,49.8C1138.3,1085.8,1147,1083.2,1154.5,1078.8z M1128.9,1060.8c-8.2,0-15.4-4-19.9-10.1
c1.4,0.4,3,0.7,4.5,0.7c8.5,0,15.4-6.9,15.4-15.4s-6.9-15.4-15.4-15.4c-1.6,0-3.1,0.2-4.5,0.7c4.5-6.1,11.8-10.1,19.9-10.1
c13.7,0,24.8,11.1,24.8,24.8C1153.7,1049.7,1142.6,1060.8,1128.9,1060.8L1128.9,1060.8z"/>
<linearGradient id="SVGID_00000127739374497564837560000013534033995177318078_" gradientUnits="userSpaceOnUse" x1="18088.9141" y1="13182.8672" x2="15383.333" y2="11899.5996" gradientTransform="matrix(7.200000e-02 0 0 7.200000e-02 0.928 1.072)">
<stop offset="0" style="stop-color:#4285F4"/>
<stop offset="1" style="stop-color:#9900FF"/>
</linearGradient>
<path style="fill:url(#SVGID_00000127739374497564837560000013534033995177318078_);" d="M1328.4,937.5c0-30.6-12.2-59.7-33.8-81.3
c-21.6-21.6-50.7-33.8-81.3-33.8c-30.6,0-59.7,12.2-81.3,33.8c-21.6,21.6-33.8,50.7-33.8,81.3v5.2c0,6.7,5.4,12.1,12.1,12.1
c6.7,0,12.1-5.4,12.1-12.1v-5.2c0-24.2,9.7-47.2,26.7-64.2c17.1-17.1,40.1-26.7,64.2-26.7s47.2,9.7,64.2,26.7
c17.1,17.1,26.7,40.1,26.7,64.2c0,6.7,5.4,12.1,12.1,12.1C1323,949.5,1328.4,944.1,1328.4,937.5z"/>
<linearGradient id="SVGID_00000026880830724572405890000002574533588083035832_" gradientUnits="userSpaceOnUse" x1="18708.3613" y1="14393.377" x2="18708.3613" y2="16782.8711" gradientTransform="matrix(7.200000e-02 0 0 7.200000e-02 0.928 1.072)">
<stop offset="0" style="stop-color:#000030"/>
<stop offset="1" style="stop-color:#4285F4"/>
</linearGradient>
<path style="fill:url(#SVGID_00000026880830724572405890000002574533588083035832_);" d="M1328.4,973.9v14.9h19.4
c6.5,0,11.8,5.3,11.8,11.8c0,6.8-4.6,12.4-11.8,12.4h-19.4v122c0,5.1,2.1,9.7,5.4,13c7.3,7.3,20.9,7.3,28.2,0
c3.3-3.3,5.4-7.9,5.4-13v-4.1c0-7.2,5.7-11.6,12.6-11.6c6.4,0,11.6,5.2,11.6,11.6v4.1c0,11.8-4.8,22.4-12.5,30.1
c-16.7,16.7-45.7,16.7-62.4,0c-7.7-7.7-12.5-18.4-12.5-30.1V973.9c0-7,5.6-11.8,12.4-11.8C1323.1,962.2,1328.3,967.4,1328.4,973.9
L1328.4,973.9z"/>
<linearGradient id="SVGID_00000018229338295230736120000011477717140636842910_" gradientUnits="userSpaceOnUse" x1="17447.4375" y1="15469.0166" x2="17540.1348" y2="16329.7832" gradientTransform="matrix(7.200000e-02 0 0 7.200000e-02 0.928 1.072)">
<stop offset="0" style="stop-color:#4285F4"/>
<stop offset="1" style="stop-color:#9900FF"/>
</linearGradient>
<path style="fill:url(#SVGID_00000018229338295230736120000011477717140636842910_);" d="M1272.6,1165.5c0,6.4-5.2,11.6-11.6,11.6
c-6.9,0-12.6-4.4-12.6-11.6c0-35.5,0-3.9,0-39.4c0-6.4,5.2-11.6,11.6-11.6c6.9,0,12.6,4.4,12.6,11.6
C1272.6,1161.6,1272.6,1130.1,1272.6,1165.5z"/>
<path class="st5" d="M707.2,1020.3v82.9h-25.1v-41.6h-54.3v41.6h-25.1v-82.9C602.7,952,707.2,951.1,707.2,1020.3z M996.8,1103.2
c37.1,0,67.2-30.1,67.2-67.2s-30.1-67.2-67.2-67.2s-67.2,30.1-67.2,67.2C929.6,1073.2,959.7,1103.2,996.8,1103.2z M996.8,1077.5
c-22.9,0-41.5-18.6-41.5-41.5c0-22.9,18.6-41.5,41.5-41.5s41.5,18.6,41.5,41.5C1038.3,1058.9,1019.8,1077.5,996.8,1077.5z
M934.1,968.8V993h-36.5v110.3h-24.2V993h-36.5v-24.2C869.3,968.8,901.7,968.8,934.1,968.8z M824.8,1051.7v-82.9h-25.1v82.9
c0,37.3-54.3,36.7-54.3,0v-82.9h-25.1v82.9C720.3,1120,824.8,1120.9,824.8,1051.7z M682.1,1037.4v-17.1c0-37.3-54.3-36.7-54.3,0
v17.1H682.1z"/>
<circle class="st6" cx="1379.5" cy="1096.4" r="12.4"/>
<circle class="st6" cx="1039.8" cy="1164.7" r="12.4"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -1,43 +1,16 @@
"use client";
import Image from "next/image";
import { useSearchParams } from "next/navigation";
import FlowEditor from '@/components/Flow';
export default function Home() {
return (
<div className="flex flex-col items-center px-12">
<div className="z-10 w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-600 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-900 dark:bg-zinc-900 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Get started by adding a&nbsp;
<code className="font-mono font-bold">node</code>
</p>
<div
className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:size-auto lg:bg-none"
>
<a
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
href="https://news.agpt.co/"
target="_blank"
rel="noopener noreferrer"
>
By{" "}
<Image
src="/autogpt_logo_dark.svg"
alt="AutoGPT Logo"
width={100}
height={24}
priority
/>
</a>
</div>
</div>
const query = useSearchParams();
<div className="w-full flex justify-center mt-10">
<FlowEditor
className="flow-container w-full min-h-[75vh] border border-gray-300 dark:border-gray-700 rounded-lg"
flowID={useSearchParams().get("flowID") ?? undefined}
/>
</div>
</div>
return (
<FlowEditor
className="flow-container w-full min-h-[86vh] border border-gray-300 dark:border-gray-700 rounded-lg bg-secondary"
flowID={query.get("flowID") ?? query.get("templateID") ?? undefined}
template={!!query.get("templateID")}
/>
);
}

View File

@@ -2,32 +2,77 @@
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -1,17 +1,12 @@
import React from 'react';
import type { Metadata } from "next";
import { ThemeProvider as NextThemeProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
import { Inter } from "next/font/google";
import Link from "next/link";
import "./globals.css";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button, buttonVariants } from "@/components/ui/button";
import {
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import { Providers } from "@/app/providers";
import {NavBar} from "@/components/NavBar";
import {cn} from "@/lib/utils";
const inter = Inter({ subsets: ["latin"] });
@@ -19,59 +14,34 @@ export const metadata: Metadata = {
title: "NextGen AutoGPT",
description: "Your one stop shop to creating AI Agents",
};
function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemeProvider {...props}>{children}</NextThemeProvider>
}
const NavBar = () => (
<nav className="bg-white dark:bg-slate-800 p-4 flex justify-between items-center shadow">
<div className="flex space-x-4">
<Link href="/monitor" className={buttonVariants({ variant: "ghost" })}>Monitor</Link>
<Link href="/build" className={buttonVariants({ variant: "ghost" })}>Build</Link>
<Link href="/backtrack" className={buttonVariants({ variant: "ghost" })}>Backtrack</Link>
<Link href="/explore" className={buttonVariants({ variant: "ghost" })}>Explore</Link>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 rounded-full">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Switch Workspace</DropdownMenuItem>
<DropdownMenuItem>Log out</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</nav>
);
export default function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="light"
disableTransitionOnChange
return (
<html lang="en">
<body className={
cn(
'antialiased transition-colors',
inter.className
)
}>
<Providers
attribute="class"
defaultTheme="light"
// Feel free to remove this line if you want to use the system theme by default
// enableSystem
disableTransitionOnChange
>
<div className="min-h-screen bg-gray-200 text-gray-900">
<NavBar />
<main className="container mx-auto p-4">
{children}
</main>
</div>
</ThemeProvider>
</body>
</html>
);
<div className="flex flex-col min-h-screen ">
<NavBar/>
<main className="flex-1 p-4 overflow-hidden">
{children}
</main>
</div>
</Providers>
</body>
</html>
);
}

View File

@@ -2,120 +2,162 @@
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import moment from 'moment';
import { ComposedChart, Legend, Line, ResponsiveContainer, Scatter, Tooltip, XAxis, YAxis } from 'recharts';
import { Pencil2Icon } from '@radix-ui/react-icons';
import AutoGPTServerAPI, { Flow, NodeExecutionResult } from '@/lib/autogpt_server_api';
import { hashString } from '@/lib/utils';
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 { 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';
const Monitor = () => {
const [flows, setFlows] = useState<Flow[]>([]);
const [flows, setFlows] = useState<GraphMeta[]>([]);
const [flowRuns, setFlowRuns] = useState<FlowRun[]>([]);
const [selectedFlow, setSelectedFlow] = useState<Flow | null>(null);
const [selectedFlow, setSelectedFlow] = useState<GraphMeta | null>(null);
const [selectedRun, setSelectedRun] = useState<FlowRun | null>(null);
const api = new AutoGPTServerAPI();
useEffect(() => fetchFlowsAndRuns(), []);
useEffect(() => {
const intervalId = setInterval(() => flows.map(f => refreshFlowRuns(f.id)), 5000);
return () => clearInterval(intervalId);
}, []);
function fetchFlowsAndRuns() {
// Fetch flow IDs
api.listFlowIDs()
.then(flowIDs => {
Promise.all(flowIDs.map(flowID => {
// Fetch flow run IDs
api.listFlowRunIDs(flowID)
.then(runIDs => {
runIDs.map(runID => {
// Fetch flow run
api.getFlowExecutionInfo(flowID, runID)
.then(execInfo => setFlowRuns(flowRuns => {
const flowRunIndex = flowRuns.findIndex(fr => fr.id == runID);
const flowRun = flowRunFromNodeExecutionResults(flowID, runID, execInfo)
if (flowRunIndex > -1) {
flowRuns.splice(flowRunIndex, 1, flowRun)
}
else {
flowRuns.push(flowRun)
}
return flowRuns
}));
});
});
// Fetch flow
return api.getFlow(flowID);
}))
.then(flows => setFlows(flows));
api.listGraphs()
.then(flows => {
setFlows(flows);
flows.map(flow => refreshFlowRuns(flow.id));
});
}
function refreshFlowRuns(flowID: string) {
// Fetch flow run IDs
api.listGraphRunIDs(flowID)
.then(runIDs => runIDs.map(runID => {
let run;
if (
(run = flowRuns.find(fr => fr.id == runID))
&& !["waiting", "running"].includes(run.status)
) {
return
}
// Fetch flow run
api.getGraphExecutionInfo(flowID, runID)
.then(execInfo => setFlowRuns(flowRuns => {
if (execInfo.length == 0) return flowRuns;
const flowRunIndex = flowRuns.findIndex(fr => fr.id == runID);
const flowRun = flowRunFromNodeExecutionResults(execInfo);
if (flowRunIndex > -1) {
flowRuns.splice(flowRunIndex, 1, flowRun)
}
else {
flowRuns.push(flowRun)
}
return [...flowRuns]
}));
}));
}
const column1 = "md:col-span-2 xl:col-span-3 xxl:col-span-2";
const column2 = "md:col-span-3 lg:col-span-2 xl:col-span-3 space-y-4";
const column3 = "col-span-full xl:col-span-4 xxl:col-span-5";
return (
<div className="grid grid-cols-1 lg:grid-cols-4 xl:grid-cols-10 gap-4">
<div className="lg:col-span-2 xl:col-span-2">
<AgentFlowList
flows={flows}
flowRuns={flowRuns}
selectedFlow={selectedFlow}
onSelectFlow={f => setSelectedFlow(f.id == selectedFlow?.id ? null : f)}
<div className="grid grid-cols-1 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10 gap-4">
<AgentFlowList
className={column1}
flows={flows}
flowRuns={flowRuns}
selectedFlow={selectedFlow}
onSelectFlow={f => {
setSelectedRun(null);
setSelectedFlow(f.id == selectedFlow?.id ? null : f);
}}
/>
<FlowRunsList
className={column2}
flows={flows}
runs={
(
selectedFlow
? flowRuns.filter(v => v.graphID == selectedFlow.id)
: flowRuns
)
.toSorted((a, b) => Number(a.startTime) - Number(b.startTime))
}
selectedRun={selectedRun}
onSelectRun={r => setSelectedRun(r.id == selectedRun?.id ? null : r)}
/>
{selectedRun && (
<FlowRunInfo
flow={selectedFlow || flows.find(f => f.id == selectedRun.graphID)!}
flowRun={selectedRun}
className={column3}
/>
</div>
<div className="lg:col-span-2 xl:col-span-2 space-y-4">
<FlowRunsList
flows={flows}
runs={
(
selectedFlow
? flowRuns.filter(v => v.flowID == selectedFlow.id)
: flowRuns
)
.toSorted((a, b) => Number(a.startTime) - Number(b.startTime))
}
) || selectedFlow && (
<FlowInfo
flow={selectedFlow}
flowRuns={flowRuns.filter(r => r.graphID == selectedFlow.id)}
className={column3}
/>
</div>
<div className="col-span-1 lg:col-span-4 xl:col-span-6">
{selectedFlow && (
<Card>
<CardHeader className="flex-row items-center justify-between space-y-0 space-x-3">
<div>
<CardTitle>{selectedFlow.name}</CardTitle>
<p className="mt-2"><code>{selectedFlow.id}</code></p>
</div>
<Link className={buttonVariants({ variant: "outline" })} href={`/build?flowID=${selectedFlow.id}`}>
<Pencil2Icon className="mr-2" /> Edit Flow
</Link>
</CardHeader>
<CardContent>
<FlowRunsStats
flows={flows}
flowRuns={flowRuns.filter(v => v.flowID == selectedFlow.id)}
/>
</CardContent>
</Card>
) || (
) || (
<Card className={`p-6 ${column3}`}>
<FlowRunsStats flows={flows} flowRuns={flowRuns} />
)}
</div>
</Card>
)}
</div>
);
};
type FlowRun = {
id: string
flowID: 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(
flowID: string, runID: string, nodeExecutionResults: NodeExecutionResult[]
nodeExecutionResults: NodeExecutionResult[]
): FlowRun {
// Determine overall status
let status: 'running' | 'waiting' | 'success' | 'failed' = 'success';
@@ -131,39 +173,103 @@ function flowRunFromNodeExecutionResults(
}
}
// Determine aggregate startTime and duration
// Determine aggregate startTime, endTime, and totalRunTime
const now = Date.now();
const startTime = Math.min(
...nodeExecutionResults.map(ner => ner.start_time?.getTime() || Date.now())
...nodeExecutionResults.map(ner => ner.add_time.getTime()), now
);
const endTime = (
['success', 'failed'].includes(status)
? Math.max(...nodeExecutionResults.map(ner => ner.end_time?.getTime() || 0))
: Date.now()
? Math.max(
...nodeExecutionResults.map(ner => ner.end_time?.getTime() || 0), startTime
)
: now
);
const duration = (endTime - startTime) / 1000; // Convert to seconds
const duration = (endTime - startTime) / 1000; // Convert to seconds
const totalRunTime = nodeExecutionResults.reduce((cum, node) => (
cum + ((node.end_time?.getTime() ?? now) - (node.start_time?.getTime() ?? now))
), 0) / 1000;
return {
id: runID,
flowID: flowID,
id: nodeExecutionResults[0].graph_exec_id,
graphID: nodeExecutionResults[0].graph_id,
graphVersion: nodeExecutionResults[0].graph_version,
status,
startTime,
endTime,
duration,
nodeExecutionResults: nodeExecutionResults
totalRunTime,
nodeExecutionResults: nodeExecutionResults,
};
}
const AgentFlowList = (
{ flows, flowRuns, selectedFlow, onSelectFlow }: {
flows: Flow[],
{ flows, flowRuns, selectedFlow, onSelectFlow, className }: {
flows: GraphMeta[],
flowRuns?: FlowRun[],
selectedFlow: Flow | null,
onSelectFlow: (f: Flow) => void,
selectedFlow: GraphMeta | null,
onSelectFlow: (f: GraphMeta) => void,
className?: string,
}
) => (
<Card>
<CardHeader>
<CardTitle>Agent Flows</CardTitle>
) => {
const [templates, setTemplates] = useState<GraphMeta[]>([]);
const api = new AutoGPTServerAPI();
useEffect(() => {
api.listTemplates().then(templates => setTemplates(templates))
}, []);
return <Card className={className}>
<CardHeader className="flex-row justify-between items-center space-x-3 space-y-0">
<CardTitle>Agents</CardTitle>
<div className="flex items-center">{/* Split "Create" button */}
<Button variant="outline" className="rounded-r-none" asChild>
<Link href="/build">Create</Link>
</Button>
<Dialog>{/* https://ui.shadcn.com/docs/components/dialog#notes */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className={"rounded-l-none border-l-0 px-2"}>
<ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DialogTrigger asChild>
<DropdownMenuItem>
<EnterIcon className="mr-2" /> Import from file
</DropdownMenuItem>
</DialogTrigger>
{templates.length > 0 && <>{/* List of templates */}
<DropdownMenuSeparator />
<DropdownMenuLabel>Use a template</DropdownMenuLabel>
{templates.map(template => (
<DropdownMenuItem
key={template.id}
onClick={() => {
api.createGraph(template.id, template.version)
.then(newGraph => {
window.location.href = `/build?flowID=${newGraph.id}`;
});
}}
>
{template.name}
</DropdownMenuItem>
))}
</>}
</DropdownMenuContent>
</DropdownMenu>
<DialogContent>
<DialogHeader className="text-lg">
Import an Agent (template) from a file
</DialogHeader>
<AgentImportForm />
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
@@ -171,21 +277,30 @@ const AgentFlowList = (
<TableHead>Name</TableHead>
{/* <TableHead>Status</TableHead> */}
{/* <TableHead>Last updated</TableHead> */}
{flowRuns && <TableHead># of runs</TableHead>}
{flowRuns && <TableHead className="md:hidden lg:table-cell"># of runs</TableHead>}
{flowRuns && <TableHead>Last run</TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{flows.map((flow) => {
let runCount, lastRun: FlowRun | null;
if (flowRuns) {
const _flowRuns = flowRuns.filter(r => r.flowID == flow.id);
runCount = _flowRuns.length;
lastRun = runCount == 0 ? null : _flowRuns.reduce(
(a, c) => a.startTime < c.startTime ? a : c
);
}
return (
{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 }) => (
<TableRow
key={flow.id}
className="cursor-pointer"
@@ -197,19 +312,19 @@ const AgentFlowList = (
{/* <TableCell>
{flow.updatedAt ?? "???"}
</TableCell> */}
{flowRuns && <TableCell>{runCount}</TableCell>}
{flowRuns && <TableCell className="md:hidden lg:table-cell">{runCount}</TableCell>}
{flowRuns && (!lastRun ? <TableCell /> :
<TableCell title={moment(lastRun.startTime).toString()}>
{moment(lastRun.startTime).fromNow()}
</TableCell>)}
</TableRow>
)
})}
))
}
</TableBody>
</Table>
</CardContent>
</Card>
);
};
const FlowStatusBadge = ({ status }: { status: "active" | "disabled" | "failing" }) => (
<Badge
@@ -224,16 +339,22 @@ const FlowStatusBadge = ({ status }: { status: "active" | "disabled" | "failing"
</Badge>
);
const FlowRunsList = ({ flows, runs }: { flows: Flow[], runs: FlowRun[] }) => (
<Card>
const FlowRunsList: React.FC<{
flows: GraphMeta[];
runs: FlowRun[];
className?: string;
selectedRun?: FlowRun | null;
onSelectRun: (r: FlowRun) => void;
}> = ({ flows, runs, selectedRun, onSelectRun, className }) => (
<Card className={className}>
<CardHeader>
<CardTitle>Flow Runs</CardTitle>
<CardTitle>Runs</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Flow</TableHead>
<TableHead>Agent</TableHead>
<TableHead>Started</TableHead>
<TableHead>Status</TableHead>
<TableHead>Duration</TableHead>
@@ -241,8 +362,13 @@ const FlowRunsList = ({ flows, runs }: { flows: Flow[], runs: FlowRun[] }) => (
</TableHeader>
<TableBody>
{runs.map((run) => (
<TableRow key={run.id}>
<TableCell>{flows.find(f => f.id == run.flowID)!.name}</TableCell>
<TableRow
key={run.id}
className="cursor-pointer"
onClick={() => onSelectRun(run)}
data-state={selectedRun?.id == run.id ? "selected" : null}
>
<TableCell>{flows.find(f => f.id == run.graphID)!.name}</TableCell>
<TableCell>{moment(run.startTime).format("HH:mm")}</TableCell>
<TableCell><FlowRunStatusBadge status={run.status} /></TableCell>
<TableCell>{formatDuration(run.duration)}</TableCell>
@@ -254,54 +380,144 @@ const FlowRunsList = ({ flows, runs }: { flows: Flow[], runs: FlowRun[] }) => (
</Card>
);
const FlowRunStatusBadge = ({ status }: { status: FlowRun['status'] }) => (
const FlowRunStatusBadge: React.FC<{
status: FlowRun['status'];
className?: string;
}> = ({ status, className }) => (
<Badge
variant="default"
className={
className={cn(
status === 'running' ? 'bg-blue-500 dark:bg-blue-700' :
status === 'waiting' ? 'bg-yellow-500 dark:bg-yellow-600' :
status === 'success' ? 'bg-green-500 dark:bg-green-600' :
'bg-red-500 dark:bg-red-700'
}
'bg-red-500 dark:bg-red-700',
className,
)}
>
{status}
</Badge>
);
const ScrollableLegend = ({ payload }) => {
return (
<div style={{
overflowX: 'auto',
overflowY: 'hidden',
whiteSpace: 'nowrap',
padding: '10px 0',
fontSize: '0.75em'
}}>
{payload.map((entry, index) => (
<span key={`item-${index}`} style={{ display: 'inline-block', marginRight: '10px' }}>
<span
style={{
display: 'inline-block',
marginRight: '5px',
width: '8px',
height: '8px',
backgroundColor: entry.color,
}}
/>
<span>{entry.value}</span>
</span>
))}
</div>
);
const FlowInfo: React.FC<React.HTMLAttributes<HTMLDivElement> & {
flow: GraphMeta;
flowRuns: FlowRun[];
flowVersion?: number | "all";
}> = ({ flow, flowRuns, flowVersion, ...props }) => {
const api = new AutoGPTServerAPI();
const [flowVersions, setFlowVersions] = useState<Graph[] | null>(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 <Card {...props}>
<CardHeader className="flex-row justify-between space-y-0 space-x-3">
<div>
<CardTitle>
{flow.name} <span className="font-light">v{flow.version}</span>
</CardTitle>
<p className="mt-2">Agent ID: <code>{flow.id}</code></p>
</div>
<div className="flex items-start space-x-2">
{(flowVersions?.length ?? 0) > 1 &&
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<ClockIcon className="mr-2" />
{selectedVersion == "all" ? "All versions" : `Version ${selectedVersion}`}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>Choose a version</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={String(selectedVersion)}
onValueChange={choice => setSelectedFlowVersion(
choice == "all" ? choice : Number(choice)
)}
>
<DropdownMenuRadioItem value="all">All versions</DropdownMenuRadioItem>
{flowVersions?.map(v =>
<DropdownMenuRadioItem key={v.version} value={v.version.toString()}>
Version {v.version}{v.is_active ? " (active)" : ""}
</DropdownMenuRadioItem>
)}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>}
<Link className={buttonVariants({ variant: "outline" })} href={`/build?flowID=${flow.id}`}>
<Pencil2Icon className="mr-2" /> Edit
</Link>
<Button
variant="outline"
className="px-2.5"
title="Export to a JSON-file"
onClick={async () => exportAsJSONFile(
safeCopyGraph(
flowVersions!.find(v => v.version == selectedFlowVersion!.version)!,
await api.getBlocks(),
),
`${flow.name}_v${selectedFlowVersion!.version}.json`
)}
>
<ExitIcon />
</Button>
</div>
</CardHeader>
<CardContent>
<FlowRunsStats
flows={[selectedFlowVersion ?? flow]}
flowRuns={flowRuns.filter(r =>
r.graphID == flow.id
&& (selectedVersion == "all" || r.graphVersion == selectedVersion)
)}
/>
</CardContent>
</Card>;
};
const FlowRunsStats = (
{ flows, flowRuns }: {
flows: Flow[],
flowRuns: FlowRun[],
const FlowRunInfo: React.FC<React.HTMLAttributes<HTMLDivElement> & {
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 <Card {...props}>
<CardHeader className="flex-row items-center justify-between space-y-0 space-x-3">
<div>
<CardTitle>
{flow.name} <span className="font-light">v{flow.version}</span>
</CardTitle>
<p className="mt-2">Agent ID: <code>{flow.id}</code></p>
<p className="mt-1">Run ID: <code>{flowRun.id}</code></p>
</div>
<Link className={buttonVariants({ variant: "outline" })} href={`/build?flowID=${flow.id}`}>
<Pencil2Icon className="mr-2" /> Edit Agent
</Link>
</CardHeader>
<CardContent>
<p><strong>Status:</strong> <FlowRunStatusBadge status={flowRun.status} /></p>
<p><strong>Started:</strong> {moment(flowRun.startTime).format('YYYY-MM-DD HH:mm:ss')}</p>
<p><strong>Finished:</strong> {moment(flowRun.endTime).format('YYYY-MM-DD HH:mm:ss')}</p>
<p><strong>Duration (run time):</strong> {flowRun.duration} ({flowRun.totalRunTime}) seconds</p>
{/* <p><strong>Total cost:</strong> €1,23</p> */}
</CardContent>
</Card>;
};
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) */
@@ -318,9 +534,9 @@ const FlowRunsStats = (
: flowRuns;
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Flow Run Stats</CardTitle>
<div className={className}>
<div className="flex flex-row items-center justify-between">
<CardTitle>{ title || "Stats" }</CardTitle>
<div className="flex space-x-2">
<Button variant="outline" size="sm" onClick={() => setStatsSince(-2*3600)}>2h</Button>
<Button variant="outline" size="sm" onClick={() => setStatsSince(-8*3600)}>8h</Button>
@@ -340,26 +556,25 @@ const FlowRunsStats = (
</Popover>
<Button variant="outline" size="sm" onClick={() => setStatsSince("dataMin")}>All</Button>
</div>
</CardHeader>
<CardContent>
<FlowRunsTimeline flows={flows} flowRuns={flowRuns} dataMin={statsSince} className={"mb-6"} />
<Card className="p-3">
<p><strong>Total runs:</strong> {filteredFlowRuns.length}</p>
<p>
<strong>Total duration:</strong> {
filteredFlowRuns.reduce((total, run) => total + run.duration, 0)
} seconds
</p>
{/* <p><strong>Total cost:</strong> €1,23</p> */}
</Card>
</CardContent>
</Card>
</div>
<FlowRunsTimeline flows={flows} flowRuns={flowRuns} dataMin={statsSince} className="mt-3" />
<hr className="my-4" />
<div>
<p><strong>Total runs:</strong> {filteredFlowRuns.length}</p>
<p>
<strong>Total run time:</strong> {
filteredFlowRuns.reduce((total, run) => total + run.totalRunTime, 0)
} seconds
</p>
{/* <p><strong>Total cost:</strong> €1,23</p> */}
</div>
</div>
)
}
const FlowRunsTimeline = (
{ flows, flowRuns, dataMin, className }: {
flows: Flow[],
flows: GraphMeta[],
flowRuns: FlowRun[],
dataMin: "dataMin" | number,
className?: string,
@@ -399,15 +614,18 @@ const FlowRunsTimeline = (
content={({ payload, label }) => {
if (payload && payload.length) {
const data: FlowRun & { time: number, _duration: number } = payload[0].payload;
const flow = flows.find(f => f.id === data.flowID);
const flow = flows.find(f => f.id === data.graphID);
return (
<Card className="p-3">
<p><strong>Flow:</strong> {flow ? flow.name : 'Unknown'}</p>
<p><strong>Start Time:</strong> {moment(data.startTime).format('YYYY-MM-DD HH:mm:ss')}</p>
<Card className="p-2 text-xs leading-normal">
<p><strong>Agent:</strong> {flow ? flow.name : 'Unknown'}</p>
<p>
<strong>Duration:</strong> {formatDuration(data.duration)}
<strong>Status:</strong>&nbsp;
<FlowRunStatusBadge status={data.status} className="px-1.5 py-0" />
</p>
<p><strong>Status:</strong> <FlowRunStatusBadge status={data.status} /></p>
<p><strong>Started:</strong> {moment(data.startTime).format('YYYY-MM-DD HH:mm:ss')}</p>
<p><strong>Duration / run time:</strong> {
formatDuration(data.duration)} / {formatDuration(data.totalRunTime)
}</p>
</Card>
);
}
@@ -417,10 +635,10 @@ const FlowRunsTimeline = (
{flows.map((flow) => (
<Scatter
key={flow.id}
data={flowRuns.filter(fr => fr.flowID == flow.id).map(fr => ({
data={flowRuns.filter(fr => fr.graphID == flow.id).map(fr => ({
...fr,
time: fr.startTime + (fr.duration * 1000),
_duration: fr.duration,
time: fr.startTime + (fr.totalRunTime * 1000),
_duration: fr.totalRunTime,
}))}
name={flow.name}
fill={`hsl(${hashString(flow.id) * 137.5 % 360}, 70%, 50%)`}
@@ -433,22 +651,56 @@ const FlowRunsTimeline = (
dataKey="_duration"
data={[
{ ...run, time: run.startTime, _duration: 0 },
{ ...run, time: run.startTime + (run.duration * 1000), _duration: run.duration }
{ ...run, time: run.endTime, _duration: run.totalRunTime }
]}
stroke={`hsl(${hashString(run.flowID) * 137.5 % 360}, 70%, 50%)`}
stroke={`hsl(${hashString(run.graphID) * 137.5 % 360}, 70%, 50%)`}
strokeWidth={2}
dot={false}
legendType="none"
/>
))}
<Legend
<Legend
content={<ScrollableLegend />}
wrapperStyle={{ bottom: 0, left: 0, right: 0 }}
wrapperStyle={{
bottom: 0,
left: 0,
right: 0,
width: "100%",
display: "flex",
justifyContent: "center",
}}
/>
</ComposedChart>
</ResponsiveContainer>
);
const ScrollableLegend: React.FC<DefaultLegendContentProps & { className?: string }> = (
{ payload, className }
) => {
return (
<div
className={cn(
"whitespace-nowrap px-4 text-sm overflow-x-auto space-x-3",
className,
)}
style={{ scrollbarWidth: "none" }}
>
{payload.map((entry, index) => {
if (entry.type == "none") return;
return (
<span key={`item-${index}`} className="inline-flex items-center">
<span
className="size-2.5 inline-block mr-1 rounded-full"
style={{backgroundColor: entry.color}}
/>
<span>{entry.value}</span>
</span>
)
})}
</div>
);
};
function formatDuration(seconds: number): string {
return (
seconds < 100

View File

@@ -0,0 +1,14 @@
'use client'
import * as React from 'react'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { ThemeProviderProps } from 'next-themes/dist/types'
import { TooltipProvider } from '@/components/ui/tooltip'
export function Providers({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider {...props}>
<TooltipProvider>{children}</TooltipProvider>
</NextThemesProvider>
)
}

View File

@@ -0,0 +1,22 @@
import { BaseEdge, ConnectionLineComponentProps, getBezierPath, Position } from "reactflow";
const ConnectionLine: React.FC<ConnectionLineComponentProps> = ({ fromPosition, fromHandle, fromX, fromY, toPosition, toX, toY }) => {
const sourceX = fromPosition === Position.Right ?
fromX + (fromHandle?.width! / 2 - 5) : fromX - (fromHandle?.width! / 2 - 5);
const [path] = getBezierPath({
sourceX: sourceX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: toX,
targetY: toY,
targetPosition: toPosition,
});
return (
<BaseEdge path={path} style={{ strokeWidth: 2, stroke: '#555' }} />
);
};
export default ConnectionLine;

View File

@@ -0,0 +1,81 @@
import React, { FC, memo, useMemo, useState } from "react";
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath, useReactFlow, XYPosition } from "reactflow";
import './customedge.css';
import { X } from 'lucide-react';
export type CustomEdgeData = {
edgeColor: string;
sourcePos?: XYPosition;
}
const CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({ id, data, selected, source, sourcePosition, sourceX, sourceY, target, targetPosition, targetX, targetY, markerEnd }) => {
const [isHovered, setIsHovered] = useState(false);
const { setEdges } = useReactFlow();
const onEdgeClick = () => {
setEdges((edges) => edges.filter((edge) => edge.id !== id));
data.clearNodesStatusAndOutput();
}
const [path, labelX, labelY] = getBezierPath({
sourceX: sourceX - 5,
sourceY,
sourcePosition,
targetX: targetX + 4,
targetY,
targetPosition,
});
// Calculate y difference between source and source node, to adjust self-loop edge
const yDifference = useMemo(() => sourceY - (data?.sourcePos?.y || 0), [data?.sourcePos?.y]);
// Define special edge path for self-loop
const edgePath = source === target ?
`M ${sourceX - 5} ${sourceY} C ${sourceX + 128} ${sourceY - yDifference - 128} ${targetX - 128} ${sourceY - yDifference - 128} ${targetX + 3}, ${targetY}` :
path;
console.table({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, path, labelX, labelY });
return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
strokeWidth: isHovered ? 3 : 2,
stroke: (data?.edgeColor ?? '#555555') + (selected || isHovered ? '' : '80')
}}
/>
<path
d={edgePath}
fill="none"
strokeOpacity={0}
strokeWidth={20}
className="react-flow__edge-interaction"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
/>
<EdgeLabelRenderer>
<div
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
pointerEvents: 'all',
}}
className="edge-label-renderer"
>
<button
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={`edge-label-button ${isHovered ? 'visible' : ''}`}
onClick={onEdgeClick}
>
<X className="size-4" />
</button>
</div>
</EdgeLabelRenderer>
</>
)
};
export const CustomEdge = memo(CustomEdgeFC);

View File

@@ -1,63 +1,67 @@
import React, { useState, useEffect, FC, memo } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import React, { useState, useEffect, FC, memo, useCallback } from 'react';
import { NodeProps, useReactFlow } from 'reactflow';
import 'reactflow/dist/style.css';
import './customnode.css';
import ModalComponent from './ModalComponent';
import { Button } from './ui/button';
import { Input } from './ui/input';
import InputModalComponent from './InputModalComponent';
import OutputModalComponent from './OutputModalComponent';
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';
type Schema = {
type: string;
properties: { [key: string]: any };
required?: string[];
enum?: string[];
items?: Schema;
additionalProperties?: { type: string };
allOf?: any[];
anyOf?: any[];
oneOf?: any[];
};
type CustomNodeData = {
export type CustomNodeData = {
blockType: string;
title: string;
inputSchema: Schema;
outputSchema: Schema;
inputSchema: BlockSchema;
outputSchema: BlockSchema;
hardcodedValues: { [key: string]: any };
setHardcodedValues: (values: { [key: string]: any }) => void;
connections: Array<{ source: string; sourceHandle: string; target: string; targetHandle: string }>;
isPropertiesOpen: boolean;
isOutputOpen: boolean;
status?: string;
output_data?: any;
block_id: string;
backend_id?: string;
errors?: { [key: string]: string | null };
setErrors: (errors: { [key: string]: string | null }) => void;
setIsAnyModalOpen?: (isOpen: boolean) => void;
};
const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
const [isPropertiesOpen, setIsPropertiesOpen] = useState(data.isPropertiesOpen || false);
const [isOutputOpen, setIsOutputOpen] = useState(data.isOutputOpen || false);
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);
const [keyValuePairs, setKeyValuePairs] = useState<{ key: string, value: string }[]>([]);
const [newKey, setNewKey] = useState<string>('');
const [newValue, setNewValue] = useState<string>('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [activeKey, setActiveKey] = useState<string | null>(null);
const [modalValue, setModalValue] = useState<string>('');
const [errors, setErrors] = useState<{ [key: string]: string | null }>({});
const [isOutputModalOpen, setIsOutputModalOpen] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const { getNode, setNodes, getEdges, setEdges } = useReactFlow();
useEffect(() => {
if (data.output_data || data.status) {
setIsPropertiesOpen(true);
setIsOutputOpen(true);
}
}, [data.output_data, data.status]);
useEffect(() => {
console.log(`Node ${id} data:`, data);
}, [id, data]);
setIsOutputOpen(data.isOutputOpen);
}, [data.isOutputOpen]);
const toggleProperties = () => {
setIsPropertiesOpen(!isPropertiesOpen);
useEffect(() => {
data.setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen);
}, [isModalOpen, isOutputModalOpen, data]);
const toggleOutput = (checked: boolean) => {
setIsOutputOpen(checked);
};
const toggleAdvancedSettings = () => {
setIsAdvancedOpen(!isAdvancedOpen);
const toggleAdvancedSettings = (checked: boolean) => {
setIsAdvancedOpen(checked);
};
const hasOptionalFields = () => {
@@ -66,33 +70,12 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
});
};
const generateHandles = (schema: Schema, type: 'source' | 'target') => {
const generateOutputHandles = (schema: BlockSchema) => {
if (!schema?.properties) return null;
const keys = Object.keys(schema.properties);
return keys.map((key) => (
<div key={key} className="handle-container">
{type === 'target' && (
<>
<Handle
type={type}
position={Position.Left}
id={key}
style={{ background: '#555', borderRadius: '50%' }}
/>
<span className="handle-label">{key}</span>
</>
)}
{type === 'source' && (
<>
<span className="handle-label">{key}</span>
<Handle
type={type}
position={Position.Right}
id={key}
style={{ background: '#555', borderRadius: '50%' }}
/>
</>
)}
<div key={key}>
<NodeHandle keyName={key} isConnected={isHandleConnected(key)} schema={schema.properties[key]} side="right" />
</div>
));
};
@@ -110,7 +93,10 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
console.log(`Updating hardcoded values for node ${id}:`, newValues);
data.setHardcodedValues(newValues);
setErrors((prevErrors) => ({ ...prevErrors, [key]: null }));
const errors = data.errors || {};
// Remove error with the same key
setNestedProperty(errors, key, null);
data.setErrors({ ...errors });
};
const getValue = (key: string) => {
@@ -122,24 +108,16 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
return data.connections && data.connections.some((conn: any) => {
if (typeof conn === 'string') {
const [source, target] = conn.split(' -> ');
return target.includes(key) && target.includes(data.title);
return (target.includes(key) && target.includes(data.title)) ||
(source.includes(key) && source.includes(data.title));
}
return conn.target === id && conn.targetHandle === key;
return (conn.target === id && conn.targetHandle === key) ||
(conn.source === id && conn.sourceHandle === key);
});
};
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);
}
};
const handleInputClick = (key: string) => {
console.log(`Opening modal for key: ${key}`);
setActiveKey(key);
const value = getValue(key);
setModalValue(typeof value === 'object' ? JSON.stringify(value, null, 2) : value);
@@ -159,311 +137,166 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
setActiveKey(null);
};
const renderInputField = (key: string, schema: any, parentKey: string = ''): JSX.Element => {
const fullKey = parentKey ? `${parentKey}.${key}` : key;
const error = errors[fullKey];
const value = getValue(fullKey);
if (isHandleConnected(fullKey)) {
return <div className="connected-input">Connected</div>;
}
const renderClickableInput = (displayValue: string) => (
<div className="clickable-input" onClick={() => handleInputClick(fullKey)}>
{displayValue}
</div>
);
if (schema.type === 'object' && schema.properties) {
return (
<div key={fullKey} className="object-input">
<strong>{key}:</strong>
{Object.entries(schema.properties).map(([propKey, propSchema]: [string, any]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
{renderInputField(propKey, propSchema, fullKey)}
</div>
))}
</div>
);
}
if (schema.type === 'object' && schema.additionalProperties) {
const objectValue = value || {};
return (
<div key={fullKey} className="object-input">
<strong>{key}:</strong>
{Object.entries(objectValue).map(([propKey, propValue]: [string, any]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
<div className="clickable-input" onClick={() => handleInputClick(`${fullKey}.${propKey}`)}>
{propKey}: {typeof propValue === 'object' ? JSON.stringify(propValue, null, 2) : propValue}
</div>
<Button onClick={() => handleInputChange(`${fullKey}.${propKey}`, undefined)} className="array-item-remove">
&times;
</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={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={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>
)}
{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 || `Enter ${key} (optional)`)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
}
if (schema.allOf) {
return (
<div key={fullKey} className="object-input">
<strong>{key}:</strong>
{schema.allOf[0].properties && Object.entries(schema.allOf[0].properties).map(([propKey, propSchema]: [string, any]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
{renderInputField(propKey, propSchema, fullKey)}
</div>
))}
</div>
);
}
if (schema.oneOf) {
return (
<div key={fullKey} className="object-input">
<strong>{key}:</strong>
{schema.oneOf[0].properties && Object.entries(schema.oneOf[0].properties).map(([propKey, propSchema]: [string, any]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
{renderInputField(propKey, propSchema, fullKey)}
</div>
))}
</div>
);
}
switch (schema.type) {
case 'string':
return schema.enum ? (
<div key={fullKey} className="input-container">
<select
value={value || ''}
onChange={(e) => handleInputChange(fullKey, e.target.value)}
className="select-input"
>
<option value="">Select {key}</option>
{schema.enum.map((option: string) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
{error && <span className="error-message">{error}</span>}
</div>
) : (
<div key={fullKey} className="input-container">
{renderClickableInput(value || `Enter ${key}`)}
{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 {key}</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 || ''}
onChange={(e) => handleInputChange(fullKey, parseFloat(e.target.value))}
className="number-input"
/>
{error && <span className="error-message">{error}</span>}
</div>
);
case 'array':
if (schema.items && schema.items.type === 'string') {
const arrayValues = value || [];
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">{error}</span>}
</div>
);
}
return null;
default:
return (
<div key={fullKey} className="input-container">
{renderClickableInput(value ? `${key} (Complex)` : `Enter ${key} (Complex)`)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
const handleOutputClick = () => {
setIsOutputModalOpen(true);
setModalValue(typeof data.output_data === 'object' ? JSON.stringify(data.output_data, null, 2) : data.output_data);
};
const validateInputs = () => {
const newErrors: { [key: string]: string | null } = {};
const validateRecursive = (schema: any, parentKey: string = '') => {
Object.entries(schema.properties).forEach(([key, propSchema]: [string, any]) => {
const fullKey = parentKey ? `${parentKey}.${key}` : key;
const value = getValue(fullKey);
const isTextTruncated = (element: HTMLElement | null): boolean => {
if (!element) return false;
return element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
};
if (propSchema.type === 'object' && propSchema.properties) {
validateRecursive(propSchema, fullKey);
} else {
if (propSchema.required && !value) {
newErrors[fullKey] = `${fullKey} is required`;
const handleHovered = () => {
setIsHovered(true);
console.log('isHovered', isHovered);
}
const handleMouseLeave = () => {
setIsHovered(false);
console.log('isHovered', isHovered);
}
const deleteNode = useCallback(() => {
console.log('Deleting node:', id);
// Get all edges connected to this node
const connectedEdges = getEdges().filter(edge => edge.source === id || edge.target === id);
// For each connected edge, update the connected node's state
connectedEdges.forEach(edge => {
const connectedNodeId = edge.source === id ? edge.target : edge.source;
const connectedNode = getNode(connectedNodeId);
if (connectedNode) {
setNodes(nodes => nodes.map(node => {
if (node.id === connectedNodeId) {
// Update the node's data to reflect the disconnection
const updatedConnections = node.data.connections.filter(
conn => !(conn.source === id || conn.target === id)
);
return {
...node,
data: {
...node.data,
connections: updatedConnections
}
};
}
}
});
};
return node;
}));
}
});
validateRecursive(data.inputSchema);
setErrors(newErrors);
return Object.values(newErrors).every((error) => error === null);
};
// Remove the node and its connected edges
setNodes(nodes => nodes.filter(node => node.id !== id));
setEdges(edges => edges.filter(edge => edge.source !== id && edge.target !== id));
}, [id, setNodes, setEdges, getNode, getEdges]);
const copyNode = useCallback(() => {
// This is a placeholder function. The actual copy functionality
// will be implemented by another team member.
console.log('Copy node:', id);
}, [id]);
return (
<div className={`custom-node dark-theme ${data.status === 'RUNNING' ? 'running' : data.status === 'COMPLETED' ? 'completed' : data.status === 'FAILED' ? 'failed' :''}`}>
<div className="node-header">
<div className="node-title">{data.blockType || data.title}</div>
<div className="node-buttons">
<Button onClick={toggleProperties} className="toggle-button">
&#9776;
</Button>
{hasOptionalFields() && (
<Button onClick={toggleAdvancedSettings} className="toggle-button">
&#9881;
</Button>
<div
className={`custom-node dark-theme ${data.status?.toLowerCase() ?? ''}`}
onMouseEnter={handleHovered}
onMouseLeave={handleMouseLeave}
>
<div className="mb-2">
<div className="text-lg font-bold">{beautifyString(data.blockType?.replace(/Block$/, '') || data.title)}</div>
<div className="node-actions">
{isHovered && (
<>
<button
className="node-action-button"
onClick={copyNode}
title="Copy node"
>
<Copy size={18} />
</button>
<button
className="node-action-button"
onClick={deleteNode}
title="Delete node"
>
<Trash2 size={18} />
</button>
</>
)}
</div>
</div>
<div className="node-content">
<div className="input-section">
<div>
{data.inputSchema &&
Object.entries(data.inputSchema.properties).map(([key, schema]) => {
const isRequired = data.inputSchema.required?.includes(key);
return (isRequired || isAdvancedOpen) && (
<div key={key}>
<div className="handle-container">
<Handle
type="target"
position={Position.Left}
id={key}
style={{ background: '#555', borderRadius: '50%' }}
/>
<span className="handle-label">{key}</span>
</div>
{renderInputField(key, schema)}
<div key={key} onMouseOver={() => { }}>
<NodeHandle keyName={key} isConnected={isHandleConnected(key)} isRequired={isRequired} schema={schema} side="left" />
{!isHandleConnected(key) &&
<NodeInputField
keyName={key}
schema={schema}
value={getValue(key)}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={data.errors?.[key]}
/>}
</div>
);
})}
</div>
<div className="output-section">
{data.outputSchema && generateHandles(data.outputSchema, 'source')}
<div>
{data.outputSchema && generateOutputHandles(data.outputSchema)}
</div>
</div>
{isPropertiesOpen && (
<div className="node-properties">
<h4>Node Output</h4>
{isOutputOpen && (
<div className="node-output" onClick={handleOutputClick}>
<p>
<strong>Status:</strong>{' '}
{typeof data.status === 'object' ? JSON.stringify(data.status) : data.status || 'N/A'}
</p>
<p>
<strong>Output Data:</strong>{' '}
{typeof data.output_data === 'object'
? JSON.stringify(data.output_data)
: data.output_data || 'N/A'}
{(() => {
const outputText = typeof data.output_data === 'object'
? JSON.stringify(data.output_data)
: data.output_data;
if (!outputText) return 'No output data';
return outputText.length > 100
? `${outputText.slice(0, 100)}... Press To Read More`
: outputText;
})()}
</p>
</div>
)}
<ModalComponent
<div className="flex items-center mt-2.5">
<Switch onCheckedChange={toggleOutput} className='custom-switch' />
<span className='m-1 mr-4'>Output</span>
{hasOptionalFields() && (
<>
<Switch onCheckedChange={toggleAdvancedSettings} className='custom-switch' />
<span className='m-1'>Advanced</span>
</>
)}
</div>
<InputModalComponent
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSave={handleModalSave}
value={modalValue}
key={activeKey}
/>
<OutputModalComponent
isOpen={isOutputModalOpen}
onClose={() => setIsOutputModalOpen(false)}
value={modalValue}
/>
</div>
);
};

View File

@@ -2,40 +2,28 @@
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import ReactFlow, {
addEdge,
applyNodeChanges,
applyEdgeChanges,
useNodesState,
useEdgesState,
Node,
Edge,
OnNodesChange,
OnEdgesChange,
OnConnect,
NodeTypes,
Connection,
EdgeTypes,
MarkerType,
} from 'reactflow';
import 'reactflow/dist/style.css';
import CustomNode from './CustomNode';
import CustomNode, { CustomNodeData } from './CustomNode';
import './flow.css';
import AutoGPTServerAPI, { Block, Flow } from '@/lib/autogpt_server_api';
import { ObjectSchema } from '@/lib/types';
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";
type CustomNodeData = {
blockType: string;
title: string;
inputSchema: ObjectSchema;
outputSchema: ObjectSchema;
hardcodedValues: { [key: string]: any };
setHardcodedValues: (values: { [key: string]: any }) => void;
connections: Array<{ source: string; sourceHandle: string; target: string; targetHandle: string }>;
isPropertiesOpen: boolean;
status?: string;
output_data?: any;
block_id: string;
backend_id?: string;
};
import { deepEquals, getTypeColor, removeEmptyStringsAndNulls, setNestedProperty } from '@/lib/utils';
import { beautifyString } from '@/lib/utils';
import { CustomEdge, CustomEdgeData } from './CustomEdge';
import ConnectionLine from './ConnectionLine';
import Ajv from 'ajv';
const Sidebar: React.FC<{ isOpen: boolean, availableNodes: Block[], addNode: (id: string, name: string) => void }> =
({ isOpen, availableNodes, addNode }) => {
@@ -58,7 +46,7 @@ const Sidebar: React.FC<{ isOpen: boolean, availableNodes: Block[], addNode: (id
/>
{filteredNodes.map((node) => (
<div key={node.id} className="sidebarNodeRowStyle dark-theme">
<span>{node.name}</span>
<span>{beautifyString(node.name).replace(/Block$/, '')}</span>
<Button onClick={() => addNode(node.id, node.name)}>Add</Button>
</div>
))}
@@ -66,21 +54,44 @@ const Sidebar: React.FC<{ isOpen: boolean, availableNodes: Block[], addNode: (id
);
};
const FlowEditor: React.FC<{ flowID?: string; className?: string }> = ({
flowID,
className,
}) => {
const [nodes, setNodes] = useState<Node<CustomNodeData>[]>([]);
const [edges, setEdges] = useState<Edge[]>([]);
const ajv = new Ajv({ strict: false, allErrors: true });
const FlowEditor: React.FC<{
flowID?: string;
template?: boolean;
className?: string;
}> = ({ flowID, template, className }) => {
const [nodes, setNodes, onNodesChange] = useNodesState<CustomNodeData>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<CustomEdgeData>([]);
const [nodeId, setNodeId] = useState<number>(1);
const [availableNodes, setAvailableNodes] = useState<Block[]>([]);
const [agentId, setAgentId] = useState<string | null>(null);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [savedAgent, setSavedAgent] = useState<Graph | null>(null);
const [agentDescription, setAgentDescription] = useState<string>('');
const [agentName, setAgentName] = useState<string>('');
const [copiedNodes, setCopiedNodes] = useState<Node<CustomNodeData>[]>([]);
const [copiedEdges, setCopiedEdges] = useState<Edge<CustomEdgeData>[]>([]);
const [isAnyModalOpen, setIsAnyModalOpen] = useState(false); // Track if any modal is open
const apiUrl = process.env.AGPT_SERVER_URL!;
const api = new AutoGPTServerAPI(apiUrl);
const api = useMemo(() => new AutoGPTServerAPI(apiUrl), [apiUrl]);
useEffect(() => {
api.connectWebSocket()
.then(() => {
console.log('WebSocket connected');
api.onWebSocketMessage('execution_event', (data) => {
updateNodesWithExecutionData([data]);
});
})
.catch((error) => {
console.error('Failed to connect WebSocket:', error);
});
return () => {
api.disconnectWebSocket();
};
}, [api]);
useEffect(() => {
api.getBlocks()
@@ -88,78 +99,109 @@ const FlowEditor: React.FC<{ flowID?: string; className?: string }> = ({
.catch();
}, []);
// Load existing flow
// Load existing graph
useEffect(() => {
if (!flowID || availableNodes.length == 0) return;
api.getFlow(flowID)
.then(flow => loadFlow(flow));
}, [flowID, availableNodes]);
(template ? api.getTemplate(flowID) : api.getGraph(flowID))
.then(graph => loadGraph(graph));
}, [flowID, template, availableNodes]);
const nodeTypes: NodeTypes = useMemo(() => ({ custom: CustomNode }), []);
const edgeTypes: EdgeTypes = useMemo(() => ({ custom: CustomEdge }), []);
const onNodesChange: OnNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[]
);
const getOutputType = (id: string, handleId: string) => {
const node = nodes.find((node) => node.id === id);
if (!node) return 'unknown';
const onEdgesChange: OnEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[]
);
const outputSchema = node.data.outputSchema;
if (!outputSchema) return 'unknown';
const onConnect: OnConnect = useCallback(
(connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
setNodes((nds) =>
nds.map((node) => {
if (node.id === connection.target) {
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;
})
);
},
[setEdges, setNodes]
);
const outputType = outputSchema.properties[handleId].type;
return outputType;
}
const onEdgesDelete = useCallback(
(edgesToDelete: Edge[]) => {
const getNodePos = (id: string) => {
const node = nodes.find((node) => node.id === id);
if (!node) return 0;
return node.position;
}
// Function to clear status, output, and close the output info dropdown of all nodes
const clearNodesStatusAndOutput = useCallback(() => {
setNodes((nds) =>
nds.map((node) => ({
...node,
data: {
...node.data,
connections: node.data.connections.filter(
(conn: any) =>
!edgesToDelete.some(
(edge) =>
edge.source === conn.source &&
edge.target === conn.target &&
edge.sourceHandle === conn.sourceHandle &&
edge.targetHandle === conn.targetHandle
)
),
status: undefined,
output_data: undefined,
isOutputOpen: false, // Close the output info dropdown
},
}))
);
},
[setNodes]
);
}, [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;
})
);
clearNodesStatusAndOutput(); // Clear status and output on connection change
}
const onEdgesDelete = useCallback(
(edgesToDelete: Edge<CustomEdgeData>[]) => {
setNodes((nds) =>
nds.map((node) => ({
...node,
data: {
...node.data,
connections: node.data.connections.filter(
(conn: any) =>
!edgesToDelete.some(
(edge) =>
edge.source === conn.source &&
edge.target === conn.target &&
edge.sourceHandle === conn.sourceHandle &&
edge.targetHandle === conn.targetHandle
)
),
},
}))
);
clearNodesStatusAndOutput(); // Clear status and output on edge deletion
},
[setNodes, clearNodesStatusAndOutput]
);
const addNode = (blockId: string, nodeType: string) => {
const nodeSchema = availableNodes.find(node => node.id === blockId);
@@ -186,25 +228,37 @@ const FlowEditor: React.FC<{ flowID?: string; className?: string }> = ({
));
},
connections: [],
isPropertiesOpen: false,
isOutputOpen: false,
block_id: blockId,
setIsAnyModalOpen: setIsAnyModalOpen, // Pass setIsAnyModalOpen function
setErrors: (errors: { [key: string]: string | null }) => {
setNodes((nds) => nds.map((node) =>
node.id === newNode.id
? { ...node, data: { ...node.data, errors } }
: node
));
}
},
};
setNodes((nds) => [...nds, newNode]);
setNodeId((prevId) => prevId + 1);
clearNodesStatusAndOutput(); // Clear status and output when a new node is added
};
function loadFlow(flow: Flow) {
setAgentId(flow.id);
function loadGraph(graph: Graph) {
setSavedAgent(graph);
setAgentName(graph.name);
setAgentDescription(graph.description);
setNodes(flow.nodes.map(node => {
setNodes(graph.nodes.map(node => {
const block = availableNodes.find(block => block.id === node.block_id)!;
const newNode = {
const newNode: Node<CustomNodeData> = {
id: node.id,
type: 'custom',
position: { x: node.metadata.position.x, y: node.metadata.position.y },
data: {
setIsAnyModalOpen: setIsAnyModalOpen,
block_id: block.id,
blockType: block.name,
title: `${block.name} ${node.id}`,
@@ -217,23 +271,44 @@ const FlowEditor: React.FC<{ flowID?: string; className?: string }> = ({
: node
));
},
connections: [],
isPropertiesOpen: false,
connections: graph.links
.filter(l => [l.source_id, l.sink_id].includes(node.id))
.map(link => ({
source: link.source_id,
sourceHandle: link.source_name,
target: link.sink_id,
targetHandle: link.sink_name,
})),
isOutputOpen: false,
setIsAnyModalOpen: setIsAnyModalOpen, // Pass setIsAnyModalOpen function
setErrors: (errors: { [key: string]: string | null }) => {
setNodes((nds) => nds.map((node) =>
node.id === newNode.id
? { ...node, data: { ...node.data, errors } }
: node
));
}
},
};
return newNode;
}));
setEdges(flow.links.map(link => ({
setEdges(graph.links.map(link => ({
id: `${link.source_id}_${link.source_name}_${link.sink_id}_${link.sink_name}`,
type: 'custom',
data: {
edgeColor: getTypeColor(getOutputType(link.source_id, link.source_name!)),
sourcePos: getNodePos(link.source_id)
},
markerEnd: { type: MarkerType.ArrowClosed, strokeWidth: 2, color: getTypeColor(getOutputType(link.source_id, link.source_name!)) },
source: link.source_id,
target: link.sink_id,
sourceHandle: link.source_name || undefined,
targetHandle: link.sink_name || undefined
})));
}) as Edge<CustomEdgeData>));
}
const prepareNodeInputData = (node: Node<CustomNodeData>, allNodes: Node<CustomNodeData>[], allEdges: Edge[]) => {
const prepareNodeInputData = (node: Node<CustomNodeData>, allNodes: Node<CustomNodeData>[], allEdges: Edge<CustomEdgeData>[]) => {
console.log("Preparing input data for node:", node.id, node.data.blockType);
const blockSchema = availableNodes.find(n => n.id === node.data.block_id)?.inputSchema;
@@ -271,130 +346,167 @@ const FlowEditor: React.FC<{ flowID?: string; className?: string }> = ({
return inputData;
};
const saveAgent = async () => {
try {
async function saveAgent(asTemplate: boolean = false) {
setNodes((nds) =>
nds.map((node) => ({
...node,
data: {
...node.data,
hardcodedValues: removeEmptyStringsAndNulls(node.data.hardcodedValues),
status: undefined,
},
}))
);
await new Promise((resolve) => setTimeout(resolve, 100));
console.log("All nodes before formatting:", nodes);
const blockIdToNodeIdMap = {};
setNodes((nds) =>
nds.map((node) => ({
...node,
data: {
...node.data,
status: null,
},
}))
);
await new Promise((resolve) => setTimeout(resolve, 100));
console.log("All nodes before formatting:", nodes);
const blockIdToNodeIdMap = {};
const formattedNodes = nodes.map(node => {
nodes.forEach(node => {
const key = `${node.data.block_id}_${node.position.x}_${node.position.y}`;
blockIdToNodeIdMap[key] = node.id;
});
const inputDefault = prepareNodeInputData(node, nodes, edges);
const inputNodes = edges
.filter(edge => edge.target === node.id)
.map(edge => ({
name: edge.targetHandle || '',
node_id: edge.source,
}));
const outputNodes = edges
.filter(edge => edge.source === node.id)
.map(edge => ({
name: edge.sourceHandle || '',
node_id: edge.target,
}));
return {
id: node.id,
block_id: node.data.block_id,
input_default: inputDefault,
input_nodes: inputNodes,
output_nodes: outputNodes,
metadata: { position: node.position }
};
const formattedNodes = nodes.map(node => {
nodes.forEach(node => {
const key = `${node.data.block_id}_${node.position.x}_${node.position.y}`;
blockIdToNodeIdMap[key] = node.id;
});
const inputDefault = prepareNodeInputData(node, nodes, edges);
const inputNodes = edges
.filter(edge => edge.target === node.id)
.map(edge => ({
name: edge.targetHandle || '',
node_id: edge.source,
}));
const links = edges.map(edge => ({
source_id: edge.source,
sink_id: edge.target,
source_name: edge.sourceHandle || '',
sink_name: edge.targetHandle || ''
}));
const outputNodes = edges
.filter(edge => edge.source === node.id)
.map(edge => ({
name: edge.sourceHandle || '',
node_id: edge.target,
}));
const payload = {
id: agentId || '',
name: agentName || 'Agent Name',
description: agentDescription || 'Agent Description',
nodes: formattedNodes,
links: links // Ensure this field is included
return {
id: node.id,
block_id: node.data.block_id,
input_default: inputDefault,
input_nodes: inputNodes,
output_nodes: outputNodes,
data: {
...node.data,
hardcodedValues: removeEmptyStringsAndNulls(node.data.hardcodedValues),
},
metadata: { position: node.position }
};
});
const createData = await api.createFlow(payload);
const newAgentId = createData.id;
setAgentId(newAgentId);
console.log('Response from the API:', JSON.stringify(createData, null, 2));
const links = edges.map(edge => ({
source_id: edge.source,
sink_id: edge.target,
source_name: edge.sourceHandle || '',
sink_name: edge.targetHandle || ''
}));
// Update the node IDs in the frontend
const updatedNodes = createData.nodes.map(backendNode => {
const key = `${backendNode.block_id}_${backendNode.metadata.position.x}_${backendNode.metadata.position.y}`;
const frontendNodeId = blockIdToNodeIdMap[key];
const frontendNode = nodes.find(node => node.id === frontendNodeId);
const payload = {
id: savedAgent?.id!,
name: agentName || 'Agent Name',
description: agentDescription || 'Agent Description',
nodes: formattedNodes,
links: links // Ensure this field is included
};
return frontendNode
? {
...frontendNode,
position: backendNode.metadata.position,
data: {
...frontendNode.data,
backend_id: backendNode.id,
},
}
: null;
}).filter(node => node !== null);
setNodes(updatedNodes);
return newAgentId;
} catch (error) {
console.error('Error running agent:', error);
if (savedAgent && deepEquals(payload, savedAgent)) {
console.debug("No need to save: Graph is the same as version on server");
return;
} else {
console.debug("Saving new Graph version; old vs new:", savedAgent, payload);
}
const newSavedAgent = savedAgent
? await (savedAgent.is_template
? api.updateTemplate(savedAgent.id, payload)
: api.updateGraph(savedAgent.id, payload))
: await (asTemplate
? api.createTemplate(payload)
: api.createGraph(payload));
console.debug('Response from the API:', newSavedAgent);
setSavedAgent(newSavedAgent);
// Update the node IDs in the frontend
const updatedNodes = newSavedAgent.nodes.map(backendNode => {
const key = `${backendNode.block_id}_${backendNode.metadata.position.x}_${backendNode.metadata.position.y}`;
const frontendNodeId = blockIdToNodeIdMap[key];
const frontendNode = nodes.find(node => node.id === frontendNodeId);
return frontendNode
? {
...frontendNode,
position: backendNode.metadata.position,
data: {
...frontendNode.data,
backend_id: backendNode.id,
},
}
: null;
}).filter(node => node !== null);
setNodes(updatedNodes);
return newSavedAgent.id;
};
const validateNodes = (): boolean => {
let isValid = true;
nodes.forEach(node => {
const validate = ajv.compile(node.data.inputSchema);
const errors = {} as { [key: string]: string | null };
// Validate values against schema using AJV
const valid = validate(node.data.hardcodedValues);
if (!valid) {
// Populate errors if validation fails
validate.errors?.forEach((error) => {
// Skip error if there's an edge connected
const path = error.instancePath || error.schemaPath;
const handle = path.split(/[\/.]/)[0];
if (node.data.connections.some(conn => conn.target === node.id || conn.targetHandle === handle)) {
return;
}
isValid = false;
if (path && error.message) {
const key = path.slice(1);
console.log("Error", key, error.message);
setNestedProperty(errors, key, error.message[0].toUpperCase() + error.message.slice(1));
} else if (error.keyword === "required") {
const key = error.params.missingProperty;
setNestedProperty(errors, key, "This field is required");
}
});
}
node.data.setErrors(errors);
});
return isValid;
};
const runAgent = async () => {
try {
const newAgentId = await saveAgent();
if (!newAgentId) {
console.error('Error saving agent');
console.error('Error saving agent; aborting run');
return;
}
const executeData = await api.executeFlow(newAgentId);
const runId = executeData.id;
if (!validateNodes()) {
console.error('Validation failed; aborting run');
return;
}
const pollExecution = async () => {
const data = await api.getFlowExecutionInfo(newAgentId, runId);
updateNodesWithExecutionData(data);
if (data.every((node) => node.status === 'COMPLETED')) {
console.log('All nodes completed execution');
} else {
setTimeout(pollExecution, 1000);
}
};
pollExecution();
api.subscribeToExecution(newAgentId);
api.runGraph(newAgentId);
} catch (error) {
console.error('Error running agent:', error);
}
};
const updateNodesWithExecutionData = (executionData: any[]) => {
const updateNodesWithExecutionData = (executionData: NodeExecutionResult[]) => {
setNodes((nds) =>
nds.map((node) => {
const nodeExecution = executionData.find((exec) => exec.node_id === node.data.backend_id);
@@ -405,7 +517,7 @@ const FlowEditor: React.FC<{ flowID?: string; className?: string }> = ({
...node.data,
status: nodeExecution.status,
output_data: nodeExecution.output_data,
isPropertiesOpen: true,
isOutputOpen: true,
},
};
}
@@ -416,31 +528,103 @@ const FlowEditor: React.FC<{ flowID?: string; className?: string }> = ({
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
const handleKeyDown = useCallback((event: KeyboardEvent) => {
if (isAnyModalOpen) return; // Prevent copy/paste if any modal is open
if (event.ctrlKey || event.metaKey) {
if (event.key === 'c' || event.key === 'C') {
// Copy selected nodes
const selectedNodes = nodes.filter(node => node.selected);
const selectedEdges = edges.filter(edge => edge.selected);
setCopiedNodes(selectedNodes);
setCopiedEdges(selectedEdges);
}
if (event.key === 'v' || event.key === 'V') {
// Paste copied nodes
if (copiedNodes.length > 0) {
const newNodes = copiedNodes.map((node, index) => {
const newNodeId = (nodeId + index).toString();
return {
...node,
id: newNodeId,
position: {
x: node.position.x + 20, // Offset pasted nodes
y: node.position.y + 20,
},
data: {
...node.data,
status: undefined, // Reset status
output_data: undefined, // Clear output data
setHardcodedValues: (values: { [key: string]: any }) => {
setNodes((nds) => nds.map((n) =>
n.id === newNodeId
? { ...n, data: { ...n.data, hardcodedValues: values } }
: n
));
},
},
};
});
const updatedNodes = nodes.map(node => ({ ...node, selected: false })); // Deselect old nodes
setNodes([...updatedNodes, ...newNodes]);
setNodeId(prevId => prevId + copiedNodes.length);
const newEdges = copiedEdges.map(edge => {
const newSourceId = newNodes.find(n => n.data.title === edge.source)?.id || edge.source;
const newTargetId = newNodes.find(n => n.data.title === edge.target)?.id || edge.target;
return {
...edge,
id: `${newSourceId}_${edge.sourceHandle}_${newTargetId}_${edge.targetHandle}_${Date.now()}`,
source: newSourceId,
target: newTargetId,
};
});
setEdges([...edges, ...newEdges]);
}
}
}
}, [nodes, edges, copiedNodes, copiedEdges, nodeId, isAnyModalOpen]);
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);
const onNodesDelete = useCallback(() => {
clearNodesStatusAndOutput();
}, [clearNodesStatusAndOutput]);
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>
<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}
edges={edges}
nodes={nodes.map(node => ({ ...node, data: { ...node.data, setIsAnyModalOpen } }))}
edges={edges.map(edge => ({...edge, data: { ...edge.data, clearNodesStatusAndOutput } }))}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
connectionLineComponent={ConnectionLine}
onNodesDelete={onNodesDelete}
onEdgesDelete={onEdgesDelete}
deleteKeyCode={["Backspace", "Delete"]}
>
<div style={{ position: 'absolute', right: 10, zIndex: 4 }}>
<Input
@@ -456,8 +640,15 @@ const FlowEditor: React.FC<{ flowID?: string; className?: string }> = ({
onChange={(e) => setAgentDescription(e.target.value)}
/>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}> {/* Added gap for spacing */}
<Button onClick={saveAgent}>Save Agent</Button>
<Button onClick={runAgent}>Save & Run Agent</Button>
<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>
</ReactFlow>

View File

@@ -1,4 +1,4 @@
import React, { FC, useEffect } from 'react';
import React, { FC, useEffect, useRef } from 'react';
import { Button } from './ui/button';
import { Textarea } from './ui/textarea';
@@ -9,12 +9,16 @@ interface ModalProps {
value: string;
}
const ModalComponent: FC<ModalProps> = ({ isOpen, onClose, onSave, value }) => {
const InputModalComponent: FC<ModalProps> = ({ isOpen, onClose, onSave, value }) => {
const [tempValue, setTempValue] = React.useState(value);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (isOpen) {
setTempValue(value);
if (textAreaRef.current) {
textAreaRef.current.select();
}
}
}, [isOpen, value]);
@@ -28,10 +32,11 @@ const ModalComponent: FC<ModalProps> = ({ isOpen, onClose, onSave, value }) => {
}
return (
<div className="fixed inset-0 bg-white bg-opacity-60 flex justify-center items-center">
<div className="nodrag fixed inset-0 bg-white bg-opacity-60 flex justify-center items-center">
<div className="bg-white p-5 rounded-lg w-[500px] max-w-[90%]">
<center><h1>Enter input text</h1></center>
<Textarea
ref={textAreaRef}
className="w-full h-[200px] p-2.5 rounded border border-[#dfdfdf] text-black bg-[#dfdfdf]"
value={tempValue}
onChange={(e) => setTempValue(e.target.value)}
@@ -45,4 +50,4 @@ const ModalComponent: FC<ModalProps> = ({ isOpen, onClose, onSave, value }) => {
);
};
export default ModalComponent;
export default InputModalComponent;

View File

@@ -0,0 +1,99 @@
import {
DropdownMenu,
DropdownMenuContent, DropdownMenuItem,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import Link from "next/link";
import { Menu } from "lucide-react";
import { Button } from "@/components/ui/button";
import React from "react";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { Pencil1Icon, TimerIcon } from "@radix-ui/react-icons";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import Image from "next/image";
export function NavBar() {
return (
<header className="sticky top-0 flex h-16 items-center gap-4 border-b bg-background px-4 md:px-6">
<div className="flex items-center gap-4 flex-1">
<Sheet>
<SheetTrigger asChild>
<Button
variant="outline"
size="icon"
className="shrink-0 md:hidden"
>
<Menu className="size-5"/>
<span className="sr-only">Toggle navigation menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left">
<nav className="grid gap-6 text-lg font-medium">
<Link
href="/monitor"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 "
>
<TimerIcon className="size-6" /> Monitor
</Link>
<Link
href="/build"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2"
>
<Pencil1Icon className="size-6"/> Build
</Link>
</nav>
</SheetContent>
</Sheet>
<nav className="hidden md:flex md:flex-row md:items-center md:gap-5 lg:gap-6">
<Link
href="/monitor"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
>
<TimerIcon className="size-4"/> Monitor
</Link>
<Link
href="/build"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
>
<Pencil1Icon className="size-4"/> Build
</Link>
</nav>
</div>
<div className="flex-1 flex justify-center relative">
<a
className="pointer-events-auto flex place-items-center gap-2"
href="https://news.agpt.co/"
target="_blank"
rel="noopener noreferrer"
>
By{" "}
<Image
src="/AUTOgpt_Logo_dark.png"
alt="AutoGPT Logo"
width={100}
height={20}
priority
/>
</a>
</div>
<div className="flex items-center gap-4 flex-1 justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="size-8">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn"/>
<AvatarFallback>CN</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Switch Workspace</DropdownMenuItem>
<DropdownMenuItem>Log out</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
);
}

View File

@@ -0,0 +1,77 @@
import { BlockSchema } from "@/lib/types";
import { beautifyString, getTypeBgColor, getTypeTextColor } from "@/lib/utils";
import { FC } from "react";
import { Handle, Position } from "reactflow";
import SchemaTooltip from "./SchemaTooltip";
type HandleProps = {
keyName: string,
schema: BlockSchema,
isConnected: boolean,
isRequired?: boolean,
side: 'left' | 'right'
}
const NodeHandle: FC<HandleProps> = ({ keyName, schema, isConnected, isRequired, side }) => {
const typeName: Record<string, string> = {
string: 'text',
number: 'number',
boolean: 'true/false',
object: 'complex',
array: 'list',
null: 'null',
};
const typeClass = `text-sm ${getTypeTextColor(schema.type)} ${side === 'left' ? 'text-left' : 'text-right'}`;
const label = (
<div className="flex flex-col flex-grow">
<span className="text-m text-gray-900 -mb-1 green">
{schema.title || beautifyString(keyName)}{isRequired ? '*' : ''}
</span>
<span className={typeClass}>{typeName[schema.type]}</span>
</div>
);
const dot = (
<div className={`w-4 h-4 m-1 ${isConnected ? getTypeBgColor(schema.type) : 'bg-gray-600'} rounded-full transition-colors duration-100 group-hover:bg-gray-300`} />
);
if (side === 'left') {
return (
<div key={keyName} className="handle-container">
<Handle
type="target"
position={Position.Left}
id={keyName}
className='group -ml-[29px]'
>
<div className="pointer-events-none flex items-center">
{dot}
{label}
</div>
</Handle>
<SchemaTooltip schema={schema} />
</div>
)
} else {
return (
<div key={keyName} className="handle-container justify-end">
<Handle
type="source"
position={Position.Right}
id={keyName}
className='group -mr-[29px]'
>
<div className="pointer-events-none flex items-center">
{label}
{dot}
</div>
</Handle>
</div >
)
}
}
export default NodeHandle;

View File

@@ -0,0 +1,296 @@
import { beautifyString } from "@/lib/utils";
import { FC, useState } from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
type BlockInputFieldProps = {
keyName: string
schema: any
parentKey?: string
value: string | Array<string> | { [key: string]: string }
handleInputClick: (key: string) => void
handleInputChange: (key: string, value: any) => void
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 fullKey = parentKey ? `${parentKey}.${key}` : key;
const error = typeof errors === 'string' ? errors : errors?.[key] ?? "";
const displayKey = schema.title || beautifyString(key);
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);
}
};
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>
)
};
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}
/>
</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;
</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>
)}
{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]) => (
<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.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]) => (
<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>
);
}
switch (schema.type) {
case 'string':
if (schema.enum) {
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':
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">
{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 ${beautifyString(displayKey)} (Complex)`)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
}
export default NodeInputField;

View File

@@ -0,0 +1,43 @@
import React, { FC, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { Button } from './ui/button';
import { Textarea } from './ui/textarea';
interface OutputModalProps {
isOpen: boolean;
onClose: () => void;
value: string;
}
const OutputModalComponent: FC<OutputModalProps> = ({ isOpen, onClose, value }) => {
const [tempValue, setTempValue] = React.useState(value);
useEffect(() => {
if (isOpen) {
setTempValue(value);
}
}, [isOpen, value]);
if (!isOpen) {
return null;
}
return createPortal(
<div className="fixed inset-0 bg-white bg-opacity-60 flex justify-center items-center z-50">
<div className="bg-white p-5 rounded-lg w-[1000px] max-w-[100%]">
<center><h1 style={{ color: 'black' }}>Full Output</h1></center>
<Textarea
className="w-full h-[400px] p-2.5 rounded border border-[#dfdfdf] text-black bg-[#dfdfdf]"
value={tempValue}
readOnly
/>
<div className="flex justify-end gap-2.5 mt-2.5">
<Button onClick={onClose}>Close</Button>
</div>
</div>
</div>,
document.body
);
};
export default OutputModalComponent;

View File

@@ -0,0 +1,30 @@
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { BlockSchema } from "@/lib/types";
import { Info } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
const SchemaTooltip: React.FC<{ schema: BlockSchema }> = ({ schema }) => {
if (!schema.description) return null;
return (
<TooltipProvider delayDuration={400}>
<Tooltip>
<TooltipTrigger asChild>
<Info className="p-1 rounded-full hover:bg-gray-300" size={24} />
</TooltipTrigger>
<TooltipContent className="max-w-xs tooltip-content">
<ReactMarkdown components={{
a: ({ node, ...props }) => <a className="text-blue-400 underline" {...props} />,
}}>{schema.description}</ReactMarkdown>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
export default SchemaTooltip;

View File

@@ -0,0 +1,180 @@
import { z } from "zod"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import React, { useState } from "react"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch"
import { Textarea } from "@/components/ui/textarea"
import AutoGPTServerAPI, { Graph, GraphCreatable } from "@/lib/autogpt-server-api"
import { cn } from "@/lib/utils"
import { EnterIcon } from "@radix-ui/react-icons"
const formSchema = z.object({
agentFile: z.instanceof(File),
agentName: z.string().min(1, "Agent name is required"),
agentDescription: z.string(),
importAsTemplate: z.boolean(),
})
export const AgentImportForm: React.FC<React.FormHTMLAttributes<HTMLFormElement>> = (
{ className, ...props }
) => {
const [agentObject, setAgentObject] = useState<GraphCreatable | null>(null)
const api = new AutoGPTServerAPI()
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
agentName: "",
agentDescription: "",
importAsTemplate: false,
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
if (!agentObject) {
form.setError("root", { message: "No Agent object to save" })
return
}
const payload: GraphCreatable = {
...agentObject,
name: values.agentName,
description: values.agentDescription,
is_active: !values.importAsTemplate,
is_template: values.importAsTemplate,
};
(values.importAsTemplate ? api.createTemplate(payload) : api.createGraph(payload))
.then((response) => {
const qID = values.importAsTemplate ? "templateID" : "flowID";
window.location.href = `/build?${qID}=${response.id}`;
})
.catch(error => {
const entity_type = values.importAsTemplate ? 'template' : 'agent';
form.setError("root", { message: `Could not create ${entity_type}: ${error}` });
})
}
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className={cn("space-y-4", className)}
{...props}
>
<FormField
control={form.control}
name="agentFile"
render={({ field }) => (
<FormItem>
<FormLabel>Agent file</FormLabel>
<FormControl className="cursor-pointer">
<Input
type="file"
accept="application/json"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
field.onChange(file)
const reader = new FileReader();
// Attach parser to file reader
reader.onload = (event) => {
try {
const obj = JSON.parse(
event.target?.result as string
);
if (
!["name", "description", "nodes", "links"]
.every(key => !!obj[key])
) {
throw new Error(
"Invalid agent object in file: "
+ JSON.stringify(obj, null, 2)
);
}
const agent = obj as Graph;
setAgentObject(agent);
form.setValue("agentName", agent.name);
form.setValue("agentDescription", agent.description);
form.setValue("importAsTemplate", agent.is_template);
} catch (error) {
console.error("Error loading agent file:", error);
}
};
// Load file
reader.readAsText(file);
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="agentName"
disabled={!agentObject}
render={({ field }) => (
<FormItem>
<FormLabel>Agent name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="agentDescription"
disabled={!agentObject}
render={({ field }) => (
<FormItem>
<FormLabel>Agent description</FormLabel>
<FormControl>
<Textarea {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="importAsTemplate"
disabled={!agentObject}
render={({ field }) => (
<FormItem>
<FormLabel>Import as</FormLabel>
<FormControl>
<div className="flex space-x-2 items-center">
<span className={field.value ? "text-gray-400 dark:text-gray-600" : ""}>Agent</span>
<Switch
disabled={field.disabled}
checked={field.value}
onCheckedChange={field.onChange}
/>
<span className={field.value ? "" : "text-gray-400 dark:text-gray-600"}>Template</span>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" disabled={!agentObject}>
<EnterIcon className="mr-2" /> Import & Edit
</Button>
</form>
</Form>
)
}

View File

@@ -0,0 +1,38 @@
.edge-label-renderer {
position: absolute;
pointer-events: all;
}
.edge-label-button {
width: 20px;
height: 20px;
background: #eee;
border: 1px solid #fff;
cursor: pointer;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
color: #555;
opacity: 0;
transition: opacity 0.2s ease-in-out, background-color 0.2s ease-in-out;
}
.edge-label-button.visible {
opacity: 1;
}
.edge-label-button:hover {
box-shadow: 0 0 6px 2px rgba(0, 0, 0, 0.08);
background: #f0f0f0;
}
.edge-label-button svg {
width: 14px;
height: 14px;
}
.react-flow__edge-interaction {
cursor: pointer;
}

View File

@@ -1,31 +1,12 @@
.custom-node {
padding: 15px;
border: 2px solid #fff;
border: 3px solid #4b5563;
border-radius: 12px;
background: #ffffff;
color: #000000;
width: 500px;
box-sizing: border-box;
transition: background-color 0.3s ease-in-out;
}
.node-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.node-title {
font-size: 18px;
font-weight: bold;
}
.toggle-button {
background: transparent;
border: none;
cursor: pointer;
color: #000000;
transition: border-color 0.3s ease-in-out;
}
.node-content {
@@ -35,33 +16,77 @@
gap: 1px;
}
.input-section {
.custom-node .mb-2 {
display: flex;
flex-direction: column;
flex: 1;
justify-content: space-between;
align-items: center;
min-height: 40px;
/* Increased to accommodate larger buttons */
margin-bottom: 10px;
}
.handle-label {
color: #000000;
margin-left: 10px;
.custom-node .mb-2 .text-lg {
flex-grow: 1;
margin-right: 10px;
}
.output-section {
.node-actions {
display: flex;
flex-direction: column;
flex: 1;
align-items: flex-end;
gap: 5px;
}
.handle-label {
margin-left: 10px;
.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;
position: relative;
margin-bottom: 5px;
margin-bottom: 0px;
padding: 5px;
min-height: 44px;
width: 100%;
height: 100%;
}
.react-flow__handle {
background: transparent;
width: auto;
height: auto;
border: 0;
position: relative;
transform: none;
}
.input-container {
@@ -72,7 +97,8 @@
padding: 5px;
width: 325px;
border-radius: 4px;
background: #d1d1d1;
background: #ffffff;
border: 1px solid #d1d1d1;
color: #000000;
cursor: pointer;
word-break: break-all;
@@ -82,6 +108,10 @@
position: relative;
}
.border-error {
border: 1px solid #d9534f;
}
.clickable-input span {
display: inline-block;
white-space: nowrap;
@@ -95,24 +125,23 @@
width: 100%;
padding: 5px;
border-radius: 4px;
border: 1px solid #555;
background: #444;
color: #e0e0e0;
border: 1px solid #000;
background: #fff;
color: #000;
}
.radio-label {
display: block;
margin: 5px 0;
color: #e0e0e0;
color: #000;
}
.number-input {
width: 100%;
padding: 5px;
border-radius: 4px;
border: 1px solid #555;
background: #444;
color: #e0e0e0;
background: #fff;
color: #000;
}
.array-item-container {
@@ -125,9 +154,9 @@
flex-grow: 1;
padding: 5px;
border-radius: 4px;
border: 1px solid #555;
background: #444;
color: #e0e0e0;
border: 1px solid #000;
background: #fff;
color: #000;
}
.array-item-remove {
@@ -150,13 +179,14 @@
margin-top: 5px;
}
.node-properties {
.node-output {
margin-top: 5px;
margin-bottom: 5px;
background: #d1d1d1;
background: #fff;
border: 1px solid #000; /* Border for output section */
padding: 10px;
border-radius: 10px;
width: 325px;
width: 100%;
}
.error-message {
@@ -167,7 +197,7 @@
.object-input {
margin-left: 10px;
border-left: 1px solid #d1d1d1;
border-left: 1px solid #000; /* Border for nested inputs */
padding-left: 10px;
}
@@ -186,37 +216,27 @@
flex-grow: 1;
}
@keyframes runningAnimation {
0% { background-color: #f39c12; }
50% { background-color: #e67e22; }
100% { background-color: #f39c12; }
/* Styles for node states */
.completed {
border-color: #27ae60; /* Green border for completed nodes */
}
.running {
animation: runningAnimation 0.5s infinite alternate;
}
/* Animation for completed status */
@keyframes completedAnimation {
0% { background-color: #27ae60; }
100% { background-color: #2ecc71; }
}
.completed {
animation: completedAnimation 0.5s infinite alternate;
}
/* Animation for failed status */
@keyframes failedAnimation {
0% { background-color: #c0392b; }
100% { background-color: #e74c3c; }
border-color: #f39c12; /* Orange border for running nodes */
}
.failed {
animation: failedAnimation 0.5s infinite alternate;
border-color: #c0392b; /* Red border for failed nodes */
}
/* Add more styles for better look */
.custom-node {
transition: all 0.3s ease-in-out;
.incomplete {
border-color: #9f14ab; /* Pink border for incomplete nodes */
}
.queued {
border-color: #25e6e6; /* Cyanic border for failed nodes */
}
.custom-switch {
padding-left: 2px;
}

View File

@@ -1,14 +1,9 @@
/* flow.css or index.css */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #121212;
color: #e0e0e0;
}
code {
@@ -91,13 +86,14 @@ input::placeholder, textarea::placeholder {
top: 0;
left: -600px;
width: 350px;
height: 100%;
height: calc(100vh - 68px); /* Full height minus top offset */
background-color: #ffffff;
color: #000000;
padding: 20px;
transition: left 0.3s ease;
z-index: 1000;
overflow-y: auto;
margin-top: 68px; /* Margin to push content below the top fixed area */
}
.sidebar.open {
@@ -130,7 +126,6 @@ input::placeholder, textarea::placeholder {
.flow-container {
width: 100%;
height: 600px; /* Adjust this height as needed */
position: relative;
}
.flow-wrapper {

View File

@@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-neutral-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg dark:border-neutral-800 dark:bg-neutral-950",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 dark:ring-offset-neutral-950 dark:focus:ring-neutral-300 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-neutral-500 dark:text-neutral-400", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -88,7 +88,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
inset && "pl-8",
className
)}
@@ -104,7 +104,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
className
)}
checked={checked}
@@ -128,7 +128,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
className
)}
{...props}

View File

@@ -0,0 +1,178 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-red-500 dark:text-red-900", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-neutral-500 dark:text-neutral-400", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-red-500 dark:text-red-900", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@@ -12,6 +12,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-gray-200 bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-800 dark:placeholder:text-gray-400 dark:focus-visible:ring-gray-300",
type == "file" ? "pt-1.5 pb-0.5" : "", // fix alignment
className
)}
ref={ref}

View File

@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@@ -0,0 +1,143 @@
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-white p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out dark:bg-neutral-950",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
},
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 dark:ring-offset-neutral-950 dark:focus:ring-neutral-300 dark:data-[state=open]:bg-neutral-800">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
</SheetPrimitive.Content>
</SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className,
)}
{...props}
/>
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold text-neutral-950 dark:text-neutral-50",
className,
)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-neutral-500 dark:text-neutral-400", className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};

View File

@@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200 dark:focus-visible:ring-neutral-300 dark:focus-visible:ring-offset-neutral-950 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=unchecked]:bg-neutral-800",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0 dark:bg-neutral-950"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@@ -0,0 +1,34 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = ({ children, delayDuration = 10 }) => (
<TooltipPrimitive.Root delayDuration={delayDuration}>
{children}
</TooltipPrimitive.Root>
);
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-neutral-900 px-3 py-1.5 text-xs text-neutral-50 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:bg-neutral-50 dark:text-neutral-900",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@@ -0,0 +1,224 @@
import {
Block,
Graph,
GraphCreatable,
GraphUpdateable,
GraphMeta,
GraphExecuteResponse,
NodeExecutionResult,
} from "./types"
export default class AutoGPTServerAPI {
private baseUrl: string;
private wsUrl: string;
private socket: WebSocket | null = null;
private messageHandlers: { [key: string]: (data: any) => void } = {};
constructor(
baseUrl: string = process.env.AGPT_SERVER_URL || "http://localhost:8000/api"
) {
this.baseUrl = baseUrl;
this.wsUrl = `ws://${new URL(this.baseUrl).host}/ws`;
}
async getBlocks(): Promise<Block[]> {
return await this._get("/blocks");
}
async listGraphs(): Promise<GraphMeta[]> {
return this._get("/graphs")
}
async listTemplates(): Promise<GraphMeta[]> {
return this._get("/templates")
}
async getGraph(id: string, version?: number): Promise<Graph> {
const query = version !== undefined ? `?version=${version}` : "";
return this._get(`/graphs/${id}` + query);
}
async getTemplate(id: string, version?: number): Promise<Graph> {
const query = version !== undefined ? `?version=${version}` : "";
return this._get(`/templates/${id}` + query);
}
async getGraphAllVersions(id: string): Promise<Graph[]> {
return this._get(`/graphs/${id}/versions`);
}
async getTemplateAllVersions(id: string): Promise<Graph[]> {
return this._get(`/templates/${id}/versions`);
}
async createGraph(graphCreateBody: GraphCreatable): Promise<Graph>;
async createGraph(fromTemplateID: string, templateVersion: number): Promise<Graph>;
async createGraph(
graphOrTemplateID: GraphCreatable | string, templateVersion?: number
): Promise<Graph> {
let requestBody: GraphCreateRequestBody;
if (typeof(graphOrTemplateID) == "string") {
if (templateVersion == undefined) {
throw new Error("templateVersion not specified")
}
requestBody = {
template_id: graphOrTemplateID,
template_version: templateVersion,
}
} else {
requestBody = { graph: graphOrTemplateID }
}
return this._request("POST", "/graphs", requestBody);
}
async createTemplate(templateCreateBody: GraphCreatable): Promise<Graph> {
const requestBody: GraphCreateRequestBody = { graph: templateCreateBody };
return this._request("POST", "/templates", requestBody);
}
async updateGraph(id: string, graph: GraphUpdateable): Promise<Graph> {
return await this._request("PUT", `/graphs/${id}`, graph);
}
async updateTemplate(id: string, template: GraphUpdateable): Promise<Graph> {
return await this._request("PUT", `/templates/${id}`, template);
}
async setGraphActiveVersion(id: string, version: number): Promise<Graph> {
return this._request(
"PUT", `/graphs/${id}/versions/active`, { active_graph_version: version }
);
}
async executeGraph(
id: string, inputData: { [key: string]: any } = {}
): Promise<GraphExecuteResponse> {
return this._request("POST", `/graphs/${id}/execute`, inputData);
}
async listGraphRunIDs(graphID: string, graphVersion?: number): Promise<string[]> {
const query = graphVersion !== undefined ? `?graph_version=${graphVersion}` : "";
return this._get(`/graphs/${graphID}/executions` + query);
}
async getGraphExecutionInfo(graphID: string, runID: string): Promise<NodeExecutionResult[]> {
return (await this._get(`/graphs/${graphID}/executions/${runID}`))
.map((result: any) => ({
...result,
add_time: new Date(result.add_time),
queue_time: result.queue_time ? new Date(result.queue_time) : undefined,
start_time: result.start_time ? new Date(result.start_time) : undefined,
end_time: result.end_time ? new Date(result.end_time) : undefined,
}));
}
private async _get(path: string) {
return this._request("GET", path);
}
private async _request(
method: "GET" | "POST" | "PUT" | "PATCH",
path: string,
payload?: { [key: string]: any },
) {
if (method != "GET") {
console.debug(`${method} ${path} payload:`, payload);
}
const response = await fetch(
this.baseUrl + path,
method != "GET" ? {
method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
} : undefined
);
const response_data = await response.json();
if (!response.ok) {
console.warn(
`${method} ${path} returned non-OK response:`, response_data.detail, response
);
throw new Error(`HTTP error ${response.status}! ${response_data.detail}`);
}
return response_data;
}
connectWebSocket(): Promise<void> {
return new Promise((resolve, reject) => {
this.socket = new WebSocket(this.wsUrl);
this.socket.onopen = () => {
console.log('WebSocket connection established');
resolve();
};
this.socket.onclose = (event) => {
console.log('WebSocket connection closed', event);
this.socket = null;
};
this.socket.onerror = (error) => {
console.error('WebSocket error:', error);
reject(error);
};
this.socket.onmessage = (event) => {
const message = JSON.parse(event.data);
if (this.messageHandlers[message.method]) {
this.messageHandlers[message.method](message.data);
}
};
});
}
disconnectWebSocket() {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.close();
}
}
sendWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
method: M, data: WebsocketMessageTypeMap[M]
) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({ method, data }));
} else {
console.error('WebSocket is not connected');
}
}
onWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
method: M, handler: (data: WebsocketMessageTypeMap[M]) => void
) {
this.messageHandlers[method] = handler;
}
subscribeToExecution(graphId: string) {
this.sendWebSocketMessage('subscribe', { graph_id: graphId });
}
runGraph(graphId: string, data: WebsocketMessageTypeMap["run_graph"]["data"] = {}) {
this.sendWebSocketMessage('run_graph', { graph_id: graphId, data });
}
}
/* *** UTILITY TYPES *** */
type GraphCreateRequestBody = {
template_id: string;
template_version: number;
} | {
graph: GraphCreatable;
}
type WebsocketMessageTypeMap = {
subscribe: { graph_id: string; };
run_graph: { graph_id: string; data: { [key: string]: any }; };
execution_event: NodeExecutionResult;
}

View File

@@ -0,0 +1,5 @@
import AutoGPTServerAPI from "./client";
export default AutoGPTServerAPI;
export * from "./types";
export * from "./utils";

View File

@@ -0,0 +1,93 @@
/* Mirror of autogpt_server/data/block.py:Block */
export type Block = {
id: string;
name: string;
description: string;
inputSchema: ObjectSchema;
outputSchema: ObjectSchema;
};
export type ObjectSchema = {
type: string;
properties: { [key: string]: any };
additionalProperties?: { type: string };
required?: string[];
};
/* Mirror of autogpt_server/data/graph.py:Node */
export type Node = {
id: string;
block_id: string;
input_default: { [key: string]: any };
input_nodes: Array<{ name: string, node_id: string }>;
output_nodes: Array<{ name: string, node_id: string }>;
metadata: {
position: { x: number; y: number; };
[key: string]: any;
};
};
/* Mirror of autogpt_server/data/graph.py:Link */
export type Link = {
id: string;
source_id: string;
sink_id: string;
source_name: string;
sink_name: string;
}
export type LinkCreatable = Omit<Link, "id"> & {
id?: string;
}
/* Mirror of autogpt_server/data/graph.py:GraphMeta */
export type GraphMeta = {
id: string;
version: number;
is_active: boolean;
is_template: boolean;
name: string;
description: string;
}
/* Mirror of autogpt_server/data/graph.py:Graph */
export type Graph = GraphMeta & {
nodes: Array<Node>;
links: Array<Link>;
};
export type GraphUpdateable = Omit<
Graph,
"version" | "is_active" | "is_template" | "links"
> & {
version?: number;
is_active?: boolean;
is_template?: boolean;
links: Array<LinkCreatable>;
}
export type GraphCreatable = Omit<GraphUpdateable, "id"> & { id?: string }
/* Derived from autogpt_server/executor/manager.py:ExecutionManager.add_execution */
export type GraphExecuteResponse = {
/** ID of the initiated run */
id: string;
/** List of node executions */
executions: Array<{ id: string, node_id: string }>;
};
/* Mirror of autogpt_server/data/execution.py:ExecutionResult */
export type NodeExecutionResult = {
graph_exec_id: string;
node_exec_id: string;
graph_id: string;
graph_version: number;
node_id: string;
status: 'INCOMPLETE' | 'QUEUED' | 'RUNNING' | 'COMPLETED' | 'FAILED';
input_data: { [key: string]: any };
output_data: { [key: string]: Array<any> };
add_time: Date;
queue_time?: Date;
start_time?: Date;
end_time?: Date;
};

View File

@@ -0,0 +1,20 @@
import { Graph, Block, Node } from "./types";
/** Creates a copy of the graph with all secrets removed */
export function safeCopyGraph(graph: Graph, block_defs: Block[]): Graph {
return {
...graph,
nodes: graph.nodes.map(node => {
const block = block_defs.find(b => b.id == node.block_id)!;
return {
...node,
input_default: Object.keys(node.input_default)
.filter(k => !block.inputSchema.properties[k].secret)
.reduce((obj: Node['input_default'], key) => {
obj[key] = node.input_default[key];
return obj;
}, {}),
}
}),
}
}

View File

@@ -1,205 +0,0 @@
import { XYPosition } from "reactflow";
import { ObjectSchema } from "./types";
export default class AutoGPTServerAPI {
private baseUrl: string;
constructor(baseUrl: string = process.env.AGPT_SERVER_URL || "http://localhost:8000") {
this.baseUrl = baseUrl;
}
async getBlocks(): Promise<Block[]> {
try {
const response = await fetch(`${this.baseUrl}/blocks`);
if (!response.ok) {
console.warn("GET /blocks returned non-OK response:", response);
throw new Error(`HTTP error ${response.status}!`);
}
return await response.json();
} catch (error) {
console.error('Error fetching blocks:', error);
throw error;
}
}
async listFlowIDs(): Promise<string[]> {
try {
const response = await fetch(`${this.baseUrl}/graphs`);
if (!response.ok) {
console.warn("GET /graphs returned non-OK response:", response);
throw new Error(`HTTP error ${response.status}!`);
}
return await response.json();
} catch (error) {
console.error('Error fetching flows:', error);
throw error;
}
}
async getFlow(id: string): Promise<Flow> {
const path = `/graphs/${id}`;
try {
const response = await fetch(this.baseUrl + path);
if (!response.ok) {
console.warn(`GET ${path} returned non-OK response:`, response);
throw new Error(`HTTP error ${response.status}!`);
}
return await response.json();
} catch (error) {
console.error('Error fetching flow:', error);
throw error;
}
}
async createFlow(flowCreateBody: FlowCreateBody): Promise<Flow> {
console.debug("POST /graphs payload:", flowCreateBody);
try {
const response = await fetch(`${this.baseUrl}/graphs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(flowCreateBody),
});
const response_data = await response.json();
if (!response.ok) {
console.warn(
`POST /graphs returned non-OK response:`, response_data.detail, response
);
throw new Error(`HTTP error ${response.status}! ${response_data.detail}`)
}
return response_data;
} catch (error) {
console.error("Error storing flow:", error);
throw error;
}
}
async executeFlow(
flowId: string, inputData: { [key: string]: any } = {}
): Promise<FlowExecuteResponse> {
const path = `/graphs/${flowId}/execute`;
console.debug(`POST ${path}`);
try {
const response = await fetch(this.baseUrl + path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(inputData),
});
const response_data = await response.json();
if (!response.ok) {
console.warn(
`POST ${path} returned non-OK response:`, response_data.detail, response
);
throw new Error(`HTTP error ${response.status}! ${response_data.detail}`)
}
return response_data;
} catch (error) {
console.error("Error executing flow:", error);
throw error;
}
}
async listFlowRunIDs(flowId: string): Promise<string[]> {
const path = `/graphs/${flowId}/executions`
try {
const response = await fetch(this.baseUrl + path);
if (!response.ok) {
console.warn(`GET ${path} returned non-OK response:`, response);
throw new Error(`HTTP error ${response.status}!`);
}
return await response.json();
} catch (error) {
console.error('Error fetching flow runs:', error);
throw error;
}
}
async getFlowExecutionInfo(flowId: string, runId: string): Promise<NodeExecutionResult[]> {
const path = `/graphs/${flowId}/executions/${runId}`;
try {
const response = await fetch(this.baseUrl + path);
if (!response.ok) {
console.warn(`GET ${path} returned non-OK response:`, response);
throw new Error(`HTTP error ${response.status}!`);
}
return (await response.json()).map((result: any) => ({
...result,
add_time: new Date(result.add_time),
queue_time: result.queue_time ? new Date(result.queue_time) : undefined,
start_time: result.start_time ? new Date(result.start_time) : undefined,
end_time: result.end_time ? new Date(result.end_time) : undefined,
}));
} catch (error) {
console.error('Error fetching execution status:', error);
throw error;
}
}
}
/* Mirror of autogpt_server/data/block.py:Block */
export type Block = {
id: string;
name: string;
description: string;
inputSchema: ObjectSchema;
outputSchema: ObjectSchema;
};
/* Mirror of autogpt_server/data/graph.py:Node */
export type Node = {
id: string;
block_id: string;
input_default: Map<string, any>;
input_nodes: Array<{ name: string, node_id: string }>;
output_nodes: Array<{ name: string, node_id: string }>;
metadata: {
position: XYPosition;
[key: string]: any;
};
};
/* Mirror of autogpt_server/data/graph.py:Link */
export type Link = {
source_id: string;
sink_id: string;
source_name: string;
sink_name: string;
}
/* Mirror of autogpt_server/data/graph.py:Graph */
export type Flow = {
id: string;
name: string;
description: string;
nodes: Array<Node>;
links: Array<Link>;
};
export type FlowCreateBody = Flow | {
id?: string;
}
/* Derived from autogpt_server/executor/manager.py:ExecutionManager.add_execution */
export type FlowExecuteResponse = {
/* ID of the initiated run */
id: string;
/* List of node executions */
executions: Array<{ id: string, node_id: string }>;
};
/* Mirror of autogpt_server/data/execution.py:ExecutionResult */
export type NodeExecutionResult = {
graph_exec_id: string;
node_exec_id: string;
node_id: string;
status: 'INCOMPLETE' | 'QUEUED' | 'RUNNING' | 'COMPLETED' | 'FAILED';
input_data: Map<string, any>;
output_data: Map<string, any[]>;
add_time: Date;
queue_time?: Date;
start_time?: Date;
end_time?: Date;
};

View File

@@ -1,6 +1,14 @@
export type ObjectSchema = {
type: string;
properties: { [key: string]: any };
additionalProperties?: { type: string };
required?: string[];
};
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

@@ -16,3 +16,143 @@ export function hashString(str: string): number {
}
return hash;
}
/** Derived from https://stackoverflow.com/a/32922084 */
export function deepEquals(x: any, y: any): boolean {
const ok = Object.keys, tx = typeof x, ty = typeof y;
return x && y && tx === ty && (
tx === 'object'
? (
ok(x).length === ok(y).length &&
ok(x).every(key => deepEquals(x[key], y[key]))
)
: (x === y)
);
}
/** Get tailwind text color class from type name */
export function getTypeTextColor(type: string | null): string {
if (type === null) return 'bg-gray-500';
return {
string: 'text-green-500',
number: 'text-blue-500',
boolean: 'text-yellow-500',
object: 'text-purple-500',
array: 'text-indigo-500',
null: 'text-gray-500',
'': 'text-gray-500',
}[type] || 'text-gray-500';
}
/** Get tailwind bg color class from type name */
export function getTypeBgColor(type: string | null): string {
if (type === null) return 'bg-gray-500';
return {
string: 'bg-green-500',
number: 'bg-blue-500',
boolean: 'bg-yellow-500',
object: 'bg-purple-500',
array: 'bg-indigo-500',
null: 'bg-gray-500',
'': 'bg-gray-500',
}[type] || 'bg-gray-500';
}
export function getTypeColor(type: string | null): string {
if (type === null) return 'bg-gray-500';
return {
string: '#22c55e',
number: '#3b82f6',
boolean: '#eab308',
object: '#a855f7',
array: '#6366f1',
null: '#6b7280',
'': '#6b7280',
}[type] || '#6b7280';
}
export function beautifyString(name: string): string {
// Regular expression to identify places to split, considering acronyms
const result = name
.replace(/([a-z])([A-Z])/g, '$1 $2') // Add space before capital letters
.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2') // Add space between acronyms and next word
.replace(/_/g, ' ') // Replace underscores with spaces
.replace(/\b\w/g, char => char.toUpperCase()); // Capitalize the first letter of each word
return applyExceptions(result);
};
const exceptionMap: Record<string, string> = {
'Auto GPT': 'AutoGPT',
'Gpt': 'GPT',
'Creds': 'Credentials',
'Id': 'ID',
'Openai': 'OpenAI',
'Api': 'API',
'Url': 'URL',
'Http': 'HTTP',
'Json': 'JSON',
};
const applyExceptions = (str: string): string => {
Object.keys(exceptionMap).forEach(key => {
const regex = new RegExp(`\\b${key}\\b`, 'g');
str = str.replace(regex, exceptionMap[key]);
});
return str;
};
export function exportAsJSONFile(obj: object, filename: string): void {
// Create downloadable blob
const jsonString = JSON.stringify(obj, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
// Trigger the browser to download the blob to a file
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Clean up
URL.revokeObjectURL(url);
}
export function setNestedProperty(obj: any, path: string, value: any) {
const keys = path.split(/[\/.]/); // Split by / or .
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!current[key] || typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
}
export function removeEmptyStringsAndNulls(obj: any): any {
if (Array.isArray(obj)) {
// If obj is an array, recursively remove empty strings and nulls from its elements
return obj
.map(item => removeEmptyStringsAndNulls(item))
.filter(item => item !== null && (typeof item !== 'string' || item.trim() !== ''));
} else if (typeof obj === 'object' && obj !== null) {
// If obj is an object, recursively remove empty strings and nulls from its properties
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (value === null || (typeof value === 'string' && value.trim() === '')) {
delete obj[key];
} else {
obj[key] = removeEmptyStringsAndNulls(value);
}
}
}
}
return obj;
}

View File

@@ -15,21 +15,65 @@ const config = {
},
},
extend: {
fontFamily: {
sans: ['var(--font-geist-sans)'],
mono: ['var(--font-geist-mono)']
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' }
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' }
}
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
}
}
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
# AutoGPT Libs
This is a new project to store shared functionality across different services in NextGen AutoGPT (e.g. authentication)

View File

@@ -0,0 +1,16 @@
import os
from dotenv import load_dotenv
load_dotenv()
class Settings:
JWT_SECRET_KEY: str = os.getenv("SUPABASE_JWT_SECRET", "")
ENABLE_AUTH: bool = os.getenv("ENABLE_AUTH", "false").lower() == "true"
JWT_ALGORITHM: str = "HS256"
@property
def is_configured(self) -> bool:
return bool(self.JWT_SECRET_KEY)
settings = Settings()

View File

@@ -0,0 +1,20 @@
import jwt
from typing import Dict, Any
from .config import settings
def parse_jwt_token(token: str) -> Dict[str, Any]:
"""
Parse and validate a JWT token.
:param token: The token to parse
:return: The decoded payload
:raises ValueError: If the token is invalid or expired
"""
try:
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
return payload
except jwt.ExpiredSignatureError:
raise ValueError("Token has expired")
except jwt.InvalidTokenError as e:
raise ValueError(f"Invalid token: {str(e)}")

View File

@@ -0,0 +1,26 @@
import logging
from fastapi import Request, HTTPException, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from .jwt_utils import parse_jwt_token
from .config import settings
security = HTTPBearer()
async def auth_middleware(request: Request):
if not settings.ENABLE_AUTH:
# If authentication is disabled, allow the request to proceed
return {}
security = HTTPBearer()
credentials = await security(request)
if not credentials:
raise HTTPException(status_code=401, detail="Authorization header is missing")
try:
payload = parse_jwt_token(credentials.credentials)
request.state.user = payload
logging.info("Token decoded successfully")
except ValueError as e:
raise HTTPException(status_code=401, detail=str(e))
return payload

37
rnd/autogpt_libs/poetry.lock generated Normal file
View File

@@ -0,0 +1,37 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "pyjwt"
version = "2.8.0"
description = "JSON Web Token implementation in Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"},
{file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"},
]
[package.extras]
crypto = ["cryptography (>=3.4.0)"]
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]]
name = "python-dotenv"
version = "1.0.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
files = [
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
]
[package.extras]
cli = ["click (>=5.0)"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<4.0"
content-hash = "7030c59d6f7c40f49ee64eb60dccc8640b35a276617f9351fb2b93d382c7113d"

Some files were not shown because too many files have changed in this diff Show More