mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Merge branch 'master' into aarushikansal-add-vector-store-support
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/1.bug.yml
vendored
17
.github/ISSUE_TEMPLATE/1.bug.yml
vendored
@@ -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
8
.github/labeler.yml
vendored
@@ -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
56
.github/workflows/autogpt-infra-ci.yml
vendored
Normal 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 }}
|
||||
184
.github/workflows/autogpt-server-ci.yml
vendored
184
.github/workflows/autogpt-server-ci.yml
vendored
@@ -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
|
||||
|
||||
55
.github/workflows/scripts/check_actions_status.py
vendored
Normal file
55
.github/workflows/scripts/check_actions_status.py
vendored
Normal 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
51
.github/workflows/workflow-checker.yml
vendored
Normal 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 }}
|
||||
4
.vscode/all-projects.code-workspace
vendored
4
.vscode/all-projects.code-workspace
vendored
@@ -33,7 +33,9 @@
|
||||
"path": ".."
|
||||
}
|
||||
],
|
||||
"settings": {},
|
||||
"settings": {
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
},
|
||||
"extensions": {
|
||||
"recommendations": [
|
||||
"charliermarsh.ruff",
|
||||
|
||||
48
README.md
48
README.md
@@ -1,17 +1,43 @@
|
||||
# AutoGPT: build & use AI agents
|
||||
# AutoGPT: Build & Use AI Agents
|
||||
|
||||
[](https://discord.gg/autogpt)  
|
||||
[](https://twitter.com/Auto_GPT)  
|
||||
[](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.
|
||||
|
||||
@@ -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
3
autogpt/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
5
autogpt/poetry.lock
generated
@@ -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
3
autogpt/scripts/llamafile/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.llamafile
|
||||
*.llamafile.exe
|
||||
llamafile.exe
|
||||
165
autogpt/scripts/llamafile/serve.py
Executable file
165
autogpt/scripts/llamafile/serve.py
Executable 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()
|
||||
2
benchmark/.vscode/settings.json
vendored
2
benchmark/.vscode/settings.json
vendored
@@ -2,5 +2,5 @@
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||
},
|
||||
"python.formatting.provider": "none"
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -4,13 +4,26 @@ Welcome to the AutoGPT Documentation.
|
||||
|
||||
The AutoGPT project consists of four main components:
|
||||
|
||||
* The [Agent](#agent) – also known as just "AutoGPT"
|
||||
* The [Benchmark](#benchmark) – AKA `agbenchmark`
|
||||
* The [Forge](#forge)
|
||||
* The [Frontend](#frontend)
|
||||
- The [Server](#server) – known as the "AutoGPT Platform"
|
||||
- The [Agent](#agent) – also known as just "AutoGPT"
|
||||
- The [Benchmark](#benchmark) – 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)**
|
||||
 | 
|
||||
**[📖 Advanced Setup](server/advanced_setup.md)**
|
||||
 | 
|
||||
**[📖 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
|
||||
|
||||
69
docs/content/server/advanced_setup.md
Normal file
69
docs/content/server/advanced_setup.md
Normal 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
|
||||
```
|
||||
218
docs/content/server/new_blocks.md
Normal file
218
docs/content/server/new_blocks.md
Normal 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
|
||||
|
||||
37
docs/content/server/ollama.md
Normal file
37
docs/content/server/ollama.md
Normal 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
|
||||
102
docs/content/server/setup.md
Normal file
102
docs/content/server/setup.md
Normal 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.
|
||||
@@ -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
3
forge/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
36
forge/forge/llm/providers/llamafile/README.md
Normal file
36
forge/forge/llm/providers/llamafile/README.md
Normal 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
|
||||
17
forge/forge/llm/providers/llamafile/__init__.py
Normal file
17
forge/forge/llm/providers/llamafile/__init__.py
Normal 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",
|
||||
]
|
||||
351
forge/forge/llm/providers/llamafile/llamafile.py
Normal file
351
forge/forge/llm/providers/llamafile/llamafile.py
Normal 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)
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
69
forge/poetry.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
Follow the on-screen instructions to complete the process.
|
||||
|
||||

|
||||

|
||||
|
||||
### 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.
|
||||

|
||||

|
||||
|
||||
```bash
|
||||
# replace the url with the one for your forked repo
|
||||
git clone https://github.com/<YOUR REPO PATH HERE>
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||

|
||||
|
||||
## 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/`.
|
||||
|
||||

|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### Stopping and Restarting Your Agent
|
||||
When needed, use Ctrl+C to end the session or use the stop command:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
---
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -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 don’t just compute; they understand, reason, and then act, drawing from a vast reservoir of information.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## The Anatomy of an LLM-Based AI Agent
|
||||
|
||||
Diving deep into the core of an LLM-based AI agent, we find it’s structured much like a human, with distinct components akin to personality, memory, thought process, and abilities. Let’s break these down:
|
||||
|
||||

|
||||

|
||||
Anatomy of an Agent from the Agent Landscape Survey
|
||||
|
||||
### **Profile**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# AutoGPT Forge: Crafting Intelligent Agent Logic
|
||||
|
||||

|
||||

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

|
||||

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

|
||||

|
||||
|
||||
4. **Select the 'WriteFile' Test**
|
||||
- Choose the 'WriteFile' test from the available options.
|
||||
|
||||
36
rnd/README.md
Normal file
36
rnd/README.md
Normal 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
0
rnd/__init__.py
Normal 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
30
rnd/autogpt_builder/.vscode/launch.json
vendored
Normal 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"
|
||||
}
|
||||
},
|
||||
|
||||
]
|
||||
}
|
||||
32
rnd/autogpt_builder/Dockerfile
Normal file
32
rnd/autogpt_builder/Dockerfile
Normal 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"]
|
||||
@@ -1,8 +0,0 @@
|
||||
const dotenv = require('dotenv');
|
||||
dotenv.config();
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
AGPT_SERVER_URL: process.env.AGPT_SERVER_URL,
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
BIN
rnd/autogpt_builder/public/AUTOgpt_Logo_dark.png
Normal file
BIN
rnd/autogpt_builder/public/AUTOgpt_Logo_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -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 |
@@ -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
|
||||
<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")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
<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
|
||||
|
||||
14
rnd/autogpt_builder/src/app/providers.tsx
Normal file
14
rnd/autogpt_builder/src/app/providers.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
22
rnd/autogpt_builder/src/components/ConnectionLine.tsx
Normal file
22
rnd/autogpt_builder/src/components/ConnectionLine.tsx
Normal 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;
|
||||
81
rnd/autogpt_builder/src/components/CustomEdge.tsx
Normal file
81
rnd/autogpt_builder/src/components/CustomEdge.tsx
Normal 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);
|
||||
@@ -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">
|
||||
×
|
||||
</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">
|
||||
×
|
||||
</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">
|
||||
☰
|
||||
</Button>
|
||||
{hasOptionalFields() && (
|
||||
<Button onClick={toggleAdvancedSettings} className="toggle-button">
|
||||
⚙
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
99
rnd/autogpt_builder/src/components/NavBar.tsx
Normal file
99
rnd/autogpt_builder/src/components/NavBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
77
rnd/autogpt_builder/src/components/NodeHandle.tsx
Normal file
77
rnd/autogpt_builder/src/components/NodeHandle.tsx
Normal 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;
|
||||
296
rnd/autogpt_builder/src/components/NodeInputField.tsx
Normal file
296
rnd/autogpt_builder/src/components/NodeInputField.tsx
Normal 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">
|
||||
×
|
||||
</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">
|
||||
×
|
||||
</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;
|
||||
43
rnd/autogpt_builder/src/components/OutputModalComponent.tsx
Normal file
43
rnd/autogpt_builder/src/components/OutputModalComponent.tsx
Normal 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;
|
||||
30
rnd/autogpt_builder/src/components/SchemaTooltip.tsx
Normal file
30
rnd/autogpt_builder/src/components/SchemaTooltip.tsx
Normal 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;
|
||||
180
rnd/autogpt_builder/src/components/agent-import-form.tsx
Normal file
180
rnd/autogpt_builder/src/components/agent-import-form.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
38
rnd/autogpt_builder/src/components/customedge.css
Normal file
38
rnd/autogpt_builder/src/components/customedge.css
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
122
rnd/autogpt_builder/src/components/ui/dialog.tsx
Normal file
122
rnd/autogpt_builder/src/components/ui/dialog.tsx
Normal 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,
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
178
rnd/autogpt_builder/src/components/ui/form.tsx
Normal file
178
rnd/autogpt_builder/src/components/ui/form.tsx
Normal 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,
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
26
rnd/autogpt_builder/src/components/ui/label.tsx
Normal file
26
rnd/autogpt_builder/src/components/ui/label.tsx
Normal 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 }
|
||||
143
rnd/autogpt_builder/src/components/ui/sheet.tsx
Normal file
143
rnd/autogpt_builder/src/components/ui/sheet.tsx
Normal 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,
|
||||
};
|
||||
29
rnd/autogpt_builder/src/components/ui/switch.tsx
Normal file
29
rnd/autogpt_builder/src/components/ui/switch.tsx
Normal 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 }
|
||||
34
rnd/autogpt_builder/src/components/ui/tooltip.tsx
Normal file
34
rnd/autogpt_builder/src/components/ui/tooltip.tsx
Normal 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 }
|
||||
224
rnd/autogpt_builder/src/lib/autogpt-server-api/client.ts
Normal file
224
rnd/autogpt_builder/src/lib/autogpt-server-api/client.ts
Normal 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;
|
||||
}
|
||||
5
rnd/autogpt_builder/src/lib/autogpt-server-api/index.ts
Normal file
5
rnd/autogpt_builder/src/lib/autogpt-server-api/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import AutoGPTServerAPI from "./client";
|
||||
|
||||
export default AutoGPTServerAPI;
|
||||
export * from "./types";
|
||||
export * from "./utils";
|
||||
93
rnd/autogpt_builder/src/lib/autogpt-server-api/types.ts
Normal file
93
rnd/autogpt_builder/src/lib/autogpt-server-api/types.ts
Normal 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;
|
||||
};
|
||||
20
rnd/autogpt_builder/src/lib/autogpt-server-api/utils.ts
Normal file
20
rnd/autogpt_builder/src/lib/autogpt-server-api/utils.ts
Normal 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;
|
||||
}, {}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
3
rnd/autogpt_libs/README.md
Normal file
3
rnd/autogpt_libs/README.md
Normal 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)
|
||||
0
rnd/autogpt_libs/autogpt_libs/__init__.py
Normal file
0
rnd/autogpt_libs/autogpt_libs/__init__.py
Normal file
0
rnd/autogpt_libs/autogpt_libs/auth/__init__.py
Normal file
0
rnd/autogpt_libs/autogpt_libs/auth/__init__.py
Normal file
16
rnd/autogpt_libs/autogpt_libs/auth/config.py
Normal file
16
rnd/autogpt_libs/autogpt_libs/auth/config.py
Normal 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()
|
||||
20
rnd/autogpt_libs/autogpt_libs/auth/jwt_utils.py
Normal file
20
rnd/autogpt_libs/autogpt_libs/auth/jwt_utils.py
Normal 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)}")
|
||||
26
rnd/autogpt_libs/autogpt_libs/auth/middleware.py
Normal file
26
rnd/autogpt_libs/autogpt_libs/auth/middleware.py
Normal 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
37
rnd/autogpt_libs/poetry.lock
generated
Normal 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
Reference in New Issue
Block a user