mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Compare commits
2 Commits
aarushikan
...
reinier/op
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0440581cf5 | ||
|
|
c3aa5fe638 |
12
.github/CODEOWNERS
vendored
12
.github/CODEOWNERS
vendored
@@ -1,7 +1,5 @@
|
||||
* @Significant-Gravitas/maintainers
|
||||
.github/workflows/ @Significant-Gravitas/devops
|
||||
forge/ @Significant-Gravitas/forge-maintainers
|
||||
benchmark/ @Significant-Gravitas/benchmark-maintainers
|
||||
frontend/ @Significant-Gravitas/frontend-maintainers
|
||||
rnd/infra @Significant-Gravitas/devops
|
||||
.github/CODEOWNERS @Significant-Gravitas/admins
|
||||
.github/workflows/ @Significant-Gravitas/devops
|
||||
autogpt/ @Significant-Gravitas/maintainers
|
||||
forge/ @Significant-Gravitas/forge-maintainers
|
||||
benchmark/ @Significant-Gravitas/benchmark-maintainers
|
||||
frontend/ @Significant-Gravitas/frontend-maintainers
|
||||
|
||||
23
.github/workflows/autogpt-infra-ci.yml
vendored
23
.github/workflows/autogpt-infra-ci.yml
vendored
@@ -23,9 +23,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: TFLint
|
||||
uses: pauloconnor/tflint-action@v0.0.2
|
||||
env:
|
||||
@@ -34,23 +31,3 @@ jobs:
|
||||
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 }}
|
||||
2
cli.py
2
cli.py
@@ -69,8 +69,6 @@ d88P 888 "Y88888 "Y888 "Y88P" "Y8888P88 888 888
|
||||
bold=True,
|
||||
)
|
||||
)
|
||||
else:
|
||||
click.echo(click.style("🎉 Setup completed!\n", fg="green"))
|
||||
|
||||
|
||||
@cli.group()
|
||||
|
||||
@@ -4,26 +4,13 @@ Welcome to the AutoGPT Documentation.
|
||||
|
||||
The AutoGPT project consists of four main components:
|
||||
|
||||
- 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)
|
||||
* 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
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -52,7 +52,7 @@ Follow these steps to create and test a new block:
|
||||
```python
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
# Unique ID for the block, used across users for templates
|
||||
# Unique ID for the block
|
||||
# you can generate this with this python one liner
|
||||
# print(__import__('uuid').uuid4())
|
||||
id="h5e7f8g9-1b2c-3d4e-5f6g-7h8i9j0k1l2m",
|
||||
@@ -147,72 +147,3 @@ This approach allows us to test the block's logic comprehensively without relyin
|
||||
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
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# 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
|
||||
@@ -1,102 +0,0 @@
|
||||
# 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.
|
||||
@@ -7,9 +7,6 @@ nav:
|
||||
|
||||
- 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
|
||||
|
||||
@@ -1,12 +1 @@
|
||||
AGPT_SERVER_URL=http://localhost:8000/api
|
||||
|
||||
## Supabase credentials
|
||||
## YOU ONLY NEED THEM IF YOU WANT TO USE SUPABASE USER AUTHENTICATION
|
||||
## If you're using self-hosted version then you most likely don't need to set this
|
||||
# NEXT_PUBLIC_SUPABASE_URL=your-project-url
|
||||
# NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
||||
|
||||
## OAuth Callback URL
|
||||
## This should be {domain}/auth/callback
|
||||
## Only used if you're using Supabase and OAuth
|
||||
AUTH_CALLBACK_URL=http://localhost:3000/auth/callback
|
||||
|
||||
@@ -2,19 +2,7 @@ This is the frontend for AutoGPT's next generation
|
||||
|
||||
## Getting Started
|
||||
|
||||
Run the following installation once.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
# or
|
||||
yarn install
|
||||
# or
|
||||
pnpm install
|
||||
# or
|
||||
bun install
|
||||
```
|
||||
|
||||
Next, run the development server:
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
@@ -30,12 +18,8 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
For subsequent runs, you do not have to `npm install` again. Simply do `npm run dev`.
|
||||
|
||||
If the project is updated via git, you will need to `npm install` after each update.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Deploy
|
||||
|
||||
TODO
|
||||
TODO
|
||||
@@ -11,20 +11,14 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-collapsible": "^1.1.0",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@supabase/ssr": "^0.4.0",
|
||||
"@supabase/supabase-js": "^2.45.0",
|
||||
"ajv": "^8.17.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
@@ -37,7 +31,6 @@
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-modal": "^3.16.1",
|
||||
"reactflow": "^11.11.4",
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function AuthErrorPage() {
|
||||
const [errorType, setErrorType] = useState<string | null>(null);
|
||||
const [errorCode, setErrorCode] = useState<string | null>(null);
|
||||
const [errorDescription, setErrorDescription] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// This code only runs on the client side
|
||||
if (typeof window !== 'undefined') {
|
||||
const hash = window.location.hash.substring(1); // Remove the leading '#'
|
||||
const params = new URLSearchParams(hash);
|
||||
|
||||
setErrorType(params.get('error'));
|
||||
setErrorCode(params.get('error_code'));
|
||||
setErrorDescription(params.get('error_description')?.replace(/\+/g, ' ') ?? null); // Replace '+' with space
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!errorType && !errorCode && !errorDescription) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Authentication Error</h1>
|
||||
{errorType && <p>Error Type: {errorType}</p>}
|
||||
{errorCode && <p>Error Code: {errorCode}</p>}
|
||||
{errorDescription && <p>Error Description: {errorDescription}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { createServerClient } from '@/lib/supabase/server'
|
||||
|
||||
// Handle the callback to complete the user session login
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams, origin } = new URL(request.url)
|
||||
const code = searchParams.get('code')
|
||||
// if "next" is in param, use it as the redirect URL
|
||||
const next = searchParams.get('next') ?? '/profile'
|
||||
|
||||
if (code) {
|
||||
const supabase = createServerClient()
|
||||
|
||||
if (!supabase) {
|
||||
return NextResponse.redirect(`${origin}/error`)
|
||||
}
|
||||
|
||||
const { data, error } = await supabase.auth.exchangeCodeForSession(code)
|
||||
// data.session?.refresh_token is available if you need to store it for later use
|
||||
if (!error) {
|
||||
const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer
|
||||
const isLocalEnv = process.env.NODE_ENV === 'development'
|
||||
if (isLocalEnv) {
|
||||
// we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
|
||||
return NextResponse.redirect(`${origin}${next}`)
|
||||
} else if (forwardedHost) {
|
||||
return NextResponse.redirect(`https://${forwardedHost}${next}`)
|
||||
} else {
|
||||
return NextResponse.redirect(`${origin}${next}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return the user to an error page with instructions
|
||||
return NextResponse.redirect(`${origin}/auth/auth-code-error`)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { type EmailOtpType } from '@supabase/supabase-js'
|
||||
import { type NextRequest } from 'next/server'
|
||||
|
||||
import { redirect } from 'next/navigation'
|
||||
import { createServerClient } from '@/lib/supabase/server'
|
||||
|
||||
// Email confirmation route
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const token_hash = searchParams.get('token_hash')
|
||||
const type = searchParams.get('type') as EmailOtpType | null
|
||||
const next = searchParams.get('next') ?? '/'
|
||||
|
||||
if (token_hash && type) {
|
||||
const supabase = createServerClient()
|
||||
|
||||
if (!supabase) {
|
||||
redirect('/error')
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.verifyOtp({
|
||||
type,
|
||||
token_hash,
|
||||
})
|
||||
if (!error) {
|
||||
// redirect user to specified redirect URL or root of app
|
||||
redirect(next)
|
||||
}
|
||||
}
|
||||
|
||||
// redirect the user to an error page with some instructions
|
||||
redirect('/error')
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import FlowEditor from '@/components/Flow';
|
||||
|
||||
@@ -7,10 +7,38 @@ export default function Home() {
|
||||
const query = useSearchParams();
|
||||
|
||||
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")}
|
||||
/>
|
||||
<div className="flex flex-col items-center min-h-screen">
|
||||
<div className="z-10 w-full flex items-center justify-between font-mono text-sm relative">
|
||||
<p className="border border-gray-600 rounded-xl pb-4 pt-4 p-4">
|
||||
Get started by adding a
|
||||
<code className="font-mono font-bold">block</code>
|
||||
</p>
|
||||
<div className="absolute top-0 right-0 p-4">
|
||||
<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={24}
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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={query.get("flowID") ?? query.get("templateID") ?? undefined}
|
||||
template={!!query.get("templateID")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function ErrorPage() {
|
||||
return <p>Sorry, something went wrong</p>
|
||||
}
|
||||
@@ -7,72 +7,3 @@
|
||||
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,12 +1,19 @@
|
||||
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 { Providers } from "@/app/providers";
|
||||
import {NavBar} from "@/components/NavBar";
|
||||
import {cn} from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { CubeIcon, Pencil1Icon, ReaderIcon, TimerIcon } from "@radix-ui/react-icons";
|
||||
|
||||
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";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -14,34 +21,60 @@ export const metadata: Metadata = {
|
||||
description: "Your one stop shop to creating AI Agents",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
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="flex flex-col min-h-screen ">
|
||||
<NavBar/>
|
||||
<main className="flex-1 p-4 overflow-hidden">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
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" })}>
|
||||
<TimerIcon className="mr-1" /> Monitor
|
||||
</Link>
|
||||
<Link href="/build" className={buttonVariants({ variant: "ghost" })}>
|
||||
<Pencil1Icon className="mr-1" /> Build
|
||||
</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,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="light"
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<div className="min-h-screen bg-gray-200 text-gray-900">
|
||||
<NavBar />
|
||||
<main className="mx-auto p-4">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
'use server'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { createServerClient } from '@/lib/supabase/server'
|
||||
import { z } from 'zod'
|
||||
|
||||
const loginFormSchema = z.object({
|
||||
email: z.string().email().min(2).max(64),
|
||||
password: z.string().min(6).max(64),
|
||||
})
|
||||
|
||||
export async function login(values: z.infer<typeof loginFormSchema>) {
|
||||
const supabase = createServerClient()
|
||||
|
||||
if (!supabase) {
|
||||
redirect('/error')
|
||||
}
|
||||
|
||||
// We are sure that the values are of the correct type because zod validates the form
|
||||
const { data, error } = await supabase.auth.signInWithPassword(values)
|
||||
|
||||
if (error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
if (data.session) {
|
||||
await supabase.auth.setSession(data.session);
|
||||
}
|
||||
|
||||
revalidatePath('/', 'layout')
|
||||
redirect('/profile')
|
||||
}
|
||||
|
||||
export async function signup(values: z.infer<typeof loginFormSchema>) {
|
||||
const supabase = createServerClient()
|
||||
|
||||
if (!supabase) {
|
||||
redirect('/error')
|
||||
}
|
||||
|
||||
// We are sure that the values are of the correct type because zod validates the form
|
||||
const { data, error } = await supabase.auth.signUp(values)
|
||||
|
||||
if (error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
if (data.session) {
|
||||
await supabase.auth.setSession(data.session);
|
||||
}
|
||||
|
||||
revalidatePath('/', 'layout')
|
||||
redirect('/profile')
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
"use client";
|
||||
import useUser from '@/hooks/useUser';
|
||||
import { login, signup } from './actions'
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { z } from "zod"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { PasswordInput } from '@/components/PasswordInput';
|
||||
import { FaGoogle, FaGithub, FaDiscord, FaSpinner } from "react-icons/fa";
|
||||
import { useState } from 'react';
|
||||
import { useSupabase } from '@/components/SupabaseProvider';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const loginFormSchema = z.object({
|
||||
email: z.string().email().min(2).max(64),
|
||||
password: z.string().min(6).max(64),
|
||||
})
|
||||
|
||||
export default function LoginPage() {
|
||||
const { supabase, isLoading: isSupabaseLoading } = useSupabase();
|
||||
const { user, isLoading: isUserLoading } = useUser();
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof loginFormSchema>>({
|
||||
resolver: zodResolver(loginFormSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
})
|
||||
|
||||
if (user) {
|
||||
console.log('User exists, redirecting to profile')
|
||||
router.push('/profile')
|
||||
}
|
||||
|
||||
if (isUserLoading || isSupabaseLoading || user) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-[80vh]">
|
||||
<FaSpinner className="mr-2 h-16 w-16 animate-spin"/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!supabase) {
|
||||
return <div>User accounts are disabled because Supabase client is unavailable</div>
|
||||
}
|
||||
|
||||
async function handleSignInWithProvider(provider: 'google' | 'github' | 'discord') {
|
||||
const { data, error } = await supabase!.auth.signInWithOAuth({
|
||||
provider: provider,
|
||||
options: {
|
||||
redirectTo: process.env.AUTH_CALLBACK_URL ?? `http://localhost:3000/auth/callback`,
|
||||
// Get Google provider_refresh_token
|
||||
// queryParams: {
|
||||
// access_type: 'offline',
|
||||
// prompt: 'consent',
|
||||
// },
|
||||
},
|
||||
})
|
||||
|
||||
if (!error) {
|
||||
setFeedback(null)
|
||||
return
|
||||
}
|
||||
setFeedback(error.message)
|
||||
}
|
||||
|
||||
const onLogin = async (data: z.infer<typeof loginFormSchema>) => {
|
||||
setIsLoading(true)
|
||||
const error = await login(data)
|
||||
setIsLoading(false)
|
||||
if (error) {
|
||||
setFeedback(error)
|
||||
return
|
||||
}
|
||||
setFeedback(null)
|
||||
}
|
||||
|
||||
const onSignup = async (data: z.infer<typeof loginFormSchema>) => {
|
||||
if (await form.trigger()) {
|
||||
setIsLoading(true)
|
||||
const error = await signup(data)
|
||||
setIsLoading(false)
|
||||
if (error) {
|
||||
setFeedback(error)
|
||||
return
|
||||
}
|
||||
setFeedback(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[80vh]">
|
||||
<div className="w-full max-w-md p-8 rounded-lg shadow-md space-y-6">
|
||||
<div className='mb-6 space-y-2'>
|
||||
<Button className="w-full" onClick={() => handleSignInWithProvider('google')} variant="outline" type="button" disabled={isLoading}>
|
||||
<FaGoogle className="mr-2 h-4 w-4" />
|
||||
Sign in with Google
|
||||
</Button>
|
||||
<Button className="w-full" onClick={() => handleSignInWithProvider('github')} variant="outline" type="button" disabled={isLoading}>
|
||||
<FaGithub className="mr-2 h-4 w-4" />
|
||||
Sign in with GitHub
|
||||
</Button>
|
||||
<Button className="w-full" onClick={() => handleSignInWithProvider('discord')} variant="outline" type="button" disabled={isLoading}>
|
||||
<FaDiscord className="mr-2 h-4 w-4" />
|
||||
Sign in with Discord
|
||||
</Button>
|
||||
</div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onLogin)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className='mb-4'>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="user@email.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput placeholder="password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Password needs to be at least 6 characters long
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className='flex w-full space-x-4 mt-6 mb-6'>
|
||||
<Button className='w-1/2 flex justify-center' type="submit" disabled={isLoading}>
|
||||
Log in
|
||||
</Button>
|
||||
<Button
|
||||
className='w-1/2 flex justify-center'
|
||||
variant='outline'
|
||||
type="button"
|
||||
onClick={form.handleSubmit(onSignup)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Sign up
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<p className='text-red-500 text-sm'>{feedback}</p>
|
||||
<p className='text-primary text-center text-sm'>
|
||||
By continuing you agree to everything
|
||||
</p>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useSupabase } from '@/components/SupabaseProvider';
|
||||
import { Button } from '@/components/ui/button'
|
||||
import useUser from '@/hooks/useUser';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FaSpinner } from 'react-icons/fa';
|
||||
|
||||
export default function PrivatePage() {
|
||||
const { user, isLoading, error } = useUser()
|
||||
const { supabase } = useSupabase()
|
||||
const router = useRouter()
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-[80vh]">
|
||||
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !user || !supabase) {
|
||||
router.push('/login')
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Hello {user.email}</p>
|
||||
<Button onClick={() => supabase.auth.signOut()}>Log out</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
'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'
|
||||
import SupabaseProvider from '@/components/SupabaseProvider'
|
||||
|
||||
export function Providers({ children, ...props }: ThemeProviderProps) {
|
||||
return (
|
||||
<NextThemesProvider {...props}>
|
||||
<SupabaseProvider>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
</SupabaseProvider>
|
||||
</NextThemesProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +1,14 @@
|
||||
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';
|
||||
import { FC, memo, useMemo } from "react";
|
||||
import { BaseEdge, EdgeProps, getBezierPath, XYPosition } from "reactflow";
|
||||
|
||||
export type CustomEdgeData = {
|
||||
edgeColor: string;
|
||||
sourcePos?: XYPosition;
|
||||
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 CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({ data, selected, source, sourcePosition, sourceX, sourceY, target, targetPosition, targetX, targetY, markerEnd }) => {
|
||||
|
||||
const onEdgeClick = () => {
|
||||
setEdges((edges) => edges.filter((edge) => edge.id !== id));
|
||||
data.clearNodesStatusAndOutput();
|
||||
}
|
||||
|
||||
const [path, labelX, labelY] = getBezierPath({
|
||||
const [path] = getBezierPath({
|
||||
sourceX: sourceX - 5,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
@@ -27,55 +18,20 @@ const CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({ id, data, selected, sourc
|
||||
});
|
||||
|
||||
// Calculate y difference between source and source node, to adjust self-loop edge
|
||||
const yDifference = useMemo(() => sourceY - (data?.sourcePos?.y || 0), [data?.sourcePos?.y]);
|
||||
const yDifference = useMemo(() => sourceY - data!.sourcePos.y, [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>
|
||||
</>
|
||||
<BaseEdge
|
||||
style={{ strokeWidth: 2, stroke: (data?.edgeColor ?? '#555555') + (selected ? '' : '80') }}
|
||||
path={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const CustomEdge = memo(CustomEdgeFC);
|
||||
export const CustomEdge = memo(CustomEdgeFC);
|
||||
|
||||
@@ -1,34 +1,26 @@
|
||||
import React, { useState, useEffect, FC, memo, useCallback, useRef } from 'react';
|
||||
import { NodeProps, useReactFlow } from 'reactflow';
|
||||
import React, { useState, useEffect, FC, memo, useRef } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
import './customnode.css';
|
||||
import InputModalComponent from './InputModalComponent';
|
||||
import OutputModalComponent from './OutputModalComponent';
|
||||
import { BlockIORootSchema, NodeExecutionResult } from '@/lib/autogpt-server-api/types';
|
||||
import { BlockSchema } from '@/lib/types';
|
||||
import { beautifyString, setNestedProperty } from '@/lib/utils';
|
||||
import { beautifyString } from '@/lib/utils';
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import NodeHandle from './NodeHandle';
|
||||
import NodeInputField from './NodeInputField';
|
||||
import { Copy, Trash2 } from 'lucide-react';
|
||||
import { history } from './history';
|
||||
|
||||
export type CustomNodeData = {
|
||||
type CustomNodeData = {
|
||||
blockType: string;
|
||||
title: string;
|
||||
inputSchema: BlockIORootSchema;
|
||||
outputSchema: BlockIORootSchema;
|
||||
inputSchema: BlockSchema;
|
||||
outputSchema: BlockSchema;
|
||||
hardcodedValues: { [key: string]: any };
|
||||
setHardcodedValues: (values: { [key: string]: any }) => void;
|
||||
connections: Array<{ source: string; sourceHandle: string; target: string; targetHandle: string }>;
|
||||
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 }) => {
|
||||
@@ -37,14 +29,9 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
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();
|
||||
|
||||
const outputDataRef = useRef<HTMLDivElement>(null);
|
||||
const isInitialSetup = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (data.output_data || data.status) {
|
||||
@@ -53,16 +40,8 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
}, [data.output_data, data.status]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsOutputOpen(data.isOutputOpen);
|
||||
}, [data.isOutputOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
data.setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen);
|
||||
}, [isModalOpen, isOutputModalOpen, data]);
|
||||
|
||||
useEffect(() => {
|
||||
isInitialSetup.current = false;
|
||||
}, []);
|
||||
console.log(`Node ${id} data:`, data);
|
||||
}, [id, data]);
|
||||
|
||||
const toggleOutput = (checked: boolean) => {
|
||||
setIsOutputOpen(checked);
|
||||
@@ -78,7 +57,7 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const generateOutputHandles = (schema: BlockIORootSchema) => {
|
||||
const generateOutputHandles = (schema: BlockSchema) => {
|
||||
if (!schema?.properties) return null;
|
||||
const keys = Object.keys(schema.properties);
|
||||
return keys.map((key) => (
|
||||
@@ -100,24 +79,12 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
current[keys[keys.length - 1]] = value;
|
||||
|
||||
console.log(`Updating hardcoded values for node ${id}:`, newValues);
|
||||
|
||||
if (!isInitialSetup.current) {
|
||||
history.push({
|
||||
type: 'UPDATE_INPUT',
|
||||
payload: { nodeId: id, oldValues: data.hardcodedValues, newValues },
|
||||
undo: () => data.setHardcodedValues(data.hardcodedValues),
|
||||
redo: () => data.setHardcodedValues(newValues),
|
||||
});
|
||||
}
|
||||
|
||||
data.setHardcodedValues(newValues);
|
||||
const errors = data.errors || {};
|
||||
// Remove error with the same key
|
||||
setNestedProperty(errors, key, null);
|
||||
data.setErrors({ ...errors });
|
||||
setErrors((prevErrors) => ({ ...prevErrors, [key]: null }));
|
||||
};
|
||||
|
||||
const getValue = (key: string) => {
|
||||
console.log(`Getting value for key: ${key}`);
|
||||
const keys = key.split('.');
|
||||
return keys.reduce((acc, k) => (acc && acc[k] !== undefined) ? acc[k] : '', data.hardcodedValues);
|
||||
};
|
||||
@@ -155,11 +122,31 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
setActiveKey(null);
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
if (propSchema.type === 'object' && propSchema.properties) {
|
||||
validateRecursive(propSchema, fullKey);
|
||||
} else {
|
||||
if (propSchema.required && !value) {
|
||||
newErrors[fullKey] = `${fullKey} is required`;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
validateRecursive(data.inputSchema);
|
||||
setErrors(newErrors);
|
||||
return Object.values(newErrors).every((error) => error === null);
|
||||
};
|
||||
|
||||
const handleOutputClick = () => {
|
||||
setIsOutputModalOpen(true);
|
||||
setModalValue(
|
||||
data.output_data ? JSON.stringify(data.output_data, null, 2) : "[no output (yet)]"
|
||||
);
|
||||
setModalValue(typeof data.output_data === 'object' ? JSON.stringify(data.output_data, null, 2) : data.output_data);
|
||||
};
|
||||
|
||||
const isTextTruncated = (element: HTMLElement | null): boolean => {
|
||||
@@ -167,86 +154,10 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
return element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
|
||||
};
|
||||
|
||||
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;
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// 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?.toLowerCase() ?? ''}`}
|
||||
onMouseEnter={handleHovered}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div className={`custom-node dark-theme ${data.status === 'RUNNING' ? 'running' : data.status === 'COMPLETED' ? 'completed' : data.status === 'FAILED' ? 'failed' : ''}`}>
|
||||
<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>
|
||||
@@ -254,17 +165,17 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
Object.entries(data.inputSchema.properties).map(([key, schema]) => {
|
||||
const isRequired = data.inputSchema.required?.includes(key);
|
||||
return (isRequired || isAdvancedOpen) && (
|
||||
<div key={key} onMouseOver={() => { }}>
|
||||
<NodeHandle keyName={key} isConnected={isHandleConnected(key)} isRequired={isRequired} schema={schema} side="left" />
|
||||
<div key={key}>
|
||||
<NodeHandle keyName={key} isConnected={isHandleConnected(key)} schema={schema} side="left" />
|
||||
{!isHandleConnected(key) &&
|
||||
<NodeInputField
|
||||
keyName={key}
|
||||
schema={schema}
|
||||
value={getValue(key)}
|
||||
handleInputClick={handleInputClick}
|
||||
handleInputChange={handleInputChange}
|
||||
errors={data.errors?.[key]}
|
||||
/>}
|
||||
<NodeInputField
|
||||
keyName={key}
|
||||
schema={schema}
|
||||
value={getValue(key)}
|
||||
handleInputClick={handleInputClick}
|
||||
handleInputChange={handleInputChange}
|
||||
errors={errors}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -285,9 +196,9 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
useNodesState,
|
||||
@@ -13,24 +13,61 @@ import ReactFlow, {
|
||||
MarkerType,
|
||||
} from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
import CustomNode, { CustomNodeData } from './CustomNode';
|
||||
import CustomNode from './CustomNode';
|
||||
import './flow.css';
|
||||
import AutoGPTServerAPI, { Block, BlockIOSchema, Graph, NodeExecutionResult } from '@/lib/autogpt-server-api';
|
||||
import { Play, Undo2, Redo2} from "lucide-react";
|
||||
import { deepEquals, getTypeColor, removeEmptyStringsAndNulls, setNestedProperty } from '@/lib/utils';
|
||||
import { history } from './history';
|
||||
import AutoGPTServerAPI, { Block, Graph, ObjectSchema } from '@/lib/autogpt-server-api';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { ChevronRight, ChevronLeft } from "lucide-react";
|
||||
import { deepEquals, getTypeColor } from '@/lib/utils';
|
||||
import { beautifyString } from '@/lib/utils';
|
||||
import { CustomEdge, CustomEdgeData } from './CustomEdge';
|
||||
import ConnectionLine from './ConnectionLine';
|
||||
import Ajv from 'ajv';
|
||||
import {Control, ControlPanel} from "@/components/edit/control/ControlPanel";
|
||||
import {SaveControl} from "@/components/edit/control/SaveControl";
|
||||
import {BlocksControl} from "@/components/edit/control/BlocksControl";
|
||||
|
||||
// This is for the history, this is the minimum distance a block must move before it is logged
|
||||
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
|
||||
const MINIMUM_MOVE_BEFORE_LOG = 50;
|
||||
|
||||
const ajv = new Ajv({ strict: false, allErrors: true });
|
||||
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 }>;
|
||||
isOutputOpen: boolean;
|
||||
status?: string;
|
||||
output_data?: any;
|
||||
block_id: string;
|
||||
backend_id?: string;
|
||||
};
|
||||
|
||||
const Sidebar: React.FC<{ isOpen: boolean, availableNodes: Block[], addNode: (id: string, name: string) => void }> =
|
||||
({ isOpen, availableNodes, addNode }) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const filteredNodes = availableNodes.filter(node =>
|
||||
node.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`sidebar dark-theme ${isOpen ? 'open' : ''}`}>
|
||||
<h3>Nodes</h3>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search nodes..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
{filteredNodes.map((node) => (
|
||||
<div key={node.id} className="sidebarNodeRowStyle dark-theme">
|
||||
<span>{beautifyString(node.name).replace(/Block$/, '')}</span>
|
||||
<Button onClick={() => addNode(node.id, node.name)}>Add</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FlowEditor: React.FC<{
|
||||
flowID?: string;
|
||||
@@ -41,17 +78,15 @@ const FlowEditor: React.FC<{
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<CustomEdgeData>([]);
|
||||
const [nodeId, setNodeId] = useState<number>(1);
|
||||
const [availableNodes, setAvailableNodes] = useState<Block[]>([]);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
const [savedAgent, setSavedAgent] = useState<Graph | null>(null);
|
||||
const [agentDescription, setAgentDescription] = useState<string>('');
|
||||
const [agentName, setAgentName] = useState<string>('');
|
||||
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 = useMemo(() => new AutoGPTServerAPI(apiUrl), [apiUrl]);
|
||||
const initialPositionRef = useRef<{ [key: string]: { x: number; y: number } }>({});
|
||||
const isDragging = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
api.connectWebSocket()
|
||||
@@ -84,97 +119,9 @@ const FlowEditor: React.FC<{
|
||||
.then(graph => loadGraph(graph));
|
||||
}, [flowID, template, availableNodes]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
const isUndo = (isMac ? event.metaKey : event.ctrlKey) && event.key === 'z';
|
||||
const isRedo = (isMac ? event.metaKey : event.ctrlKey) && (event.key === 'y' || (event.shiftKey && event.key === 'Z'));
|
||||
|
||||
if (isUndo) {
|
||||
event.preventDefault();
|
||||
handleUndo();
|
||||
}
|
||||
|
||||
if (isRedo) {
|
||||
event.preventDefault();
|
||||
handleRedo();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
const nodeTypes: NodeTypes = useMemo(() => ({ custom: CustomNode }), []);
|
||||
const edgeTypes: EdgeTypes = useMemo(() => ({ custom: CustomEdge }), []);
|
||||
|
||||
const onNodesChangeStart = (event: MouseEvent, node: Node) => {
|
||||
initialPositionRef.current[node.id] = { ...node.position };
|
||||
isDragging.current = true;
|
||||
};
|
||||
|
||||
const onNodesChangeEnd = (event: MouseEvent, node: Node | null) => {
|
||||
if (!node) return;
|
||||
|
||||
isDragging.current = false;
|
||||
const oldPosition = initialPositionRef.current[node.id];
|
||||
const newPosition = node.position;
|
||||
|
||||
// Calculate the movement distance
|
||||
if (!oldPosition || !newPosition) return;
|
||||
|
||||
const distanceMoved = Math.sqrt(
|
||||
Math.pow(newPosition.x - oldPosition.x, 2) +
|
||||
Math.pow(newPosition.y - oldPosition.y, 2)
|
||||
);
|
||||
|
||||
if (distanceMoved > MINIMUM_MOVE_BEFORE_LOG) { // Minimum movement threshold
|
||||
history.push({
|
||||
type: 'UPDATE_NODE_POSITION',
|
||||
payload: { nodeId: node.id, oldPosition, newPosition },
|
||||
undo: () => setNodes((nds) => nds.map(n => n.id === node.id ? { ...n, position: oldPosition } : n)),
|
||||
redo: () => setNodes((nds) => nds.map(n => n.id === node.id ? { ...n, position: newPosition } : n)),
|
||||
});
|
||||
}
|
||||
delete initialPositionRef.current[node.id];
|
||||
};
|
||||
|
||||
|
||||
const updateNodesOnEdgeChange = (edge: Edge<CustomEdgeData>, action: 'add' | 'remove') => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === edge.source || node.id === edge.target) {
|
||||
const connections = action === 'add'
|
||||
? [
|
||||
...node.data.connections,
|
||||
{
|
||||
source: edge.source,
|
||||
sourceHandle: edge.sourceHandle!,
|
||||
target: edge.target,
|
||||
targetHandle: edge.targetHandle!,
|
||||
}
|
||||
]
|
||||
: node.data.connections.filter(
|
||||
(conn) =>
|
||||
!(conn.source === edge.source && conn.target === edge.target && conn.sourceHandle === edge.sourceHandle && conn.targetHandle === edge.targetHandle)
|
||||
);
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
connections,
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getOutputType = (id: string, handleId: string) => {
|
||||
const node = nodes.find((node) => node.id === id);
|
||||
if (!node) return 'unknown';
|
||||
@@ -182,9 +129,8 @@ const FlowEditor: React.FC<{
|
||||
const outputSchema = node.data.outputSchema;
|
||||
if (!outputSchema) return 'unknown';
|
||||
|
||||
const outputHandle = outputSchema.properties[handleId];
|
||||
if (!("type" in outputHandle)) return "unknown";
|
||||
return outputHandle.type;
|
||||
const outputType = outputSchema.properties[handleId].type;
|
||||
return outputType;
|
||||
}
|
||||
|
||||
const getNodePos = (id: string) => {
|
||||
@@ -194,77 +140,39 @@ const FlowEditor: React.FC<{
|
||||
return node.position;
|
||||
}
|
||||
|
||||
// Function to clear status, output, and close the output info dropdown of all nodes
|
||||
const clearNodesStatusAndOutput = useCallback(() => {
|
||||
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) => ({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
status: undefined,
|
||||
output_data: undefined,
|
||||
isOutputOpen: false, // Close the output info dropdown
|
||||
},
|
||||
}))
|
||||
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;
|
||||
})
|
||||
);
|
||||
}, [setNodes]);
|
||||
|
||||
const onConnect: OnConnect = useCallback(
|
||||
(connection: Connection) => {
|
||||
const edgeColor = getTypeColor(getOutputType(connection.source!, connection.sourceHandle!));
|
||||
const sourcePos = getNodePos(connection.source!)
|
||||
console.log('sourcePos', sourcePos);
|
||||
const newEdge = {
|
||||
id: `${connection.source}_${connection.sourceHandle}_${connection.target}_${connection.targetHandle}`,
|
||||
type: 'custom',
|
||||
markerEnd: { type: MarkerType.ArrowClosed, strokeWidth: 2, color: edgeColor },
|
||||
data: { edgeColor, sourcePos },
|
||||
...connection
|
||||
};
|
||||
|
||||
setEdges((eds) => {
|
||||
const newEdges = addEdge(newEdge, eds);
|
||||
history.push({
|
||||
type: 'ADD_EDGE',
|
||||
payload: newEdge,
|
||||
undo: () => {
|
||||
setEdges((prevEdges) => prevEdges.filter(edge => edge.id !== newEdge.id));
|
||||
updateNodesOnEdgeChange(newEdge, 'remove');
|
||||
},
|
||||
redo: () => {
|
||||
setEdges((prevEdges) => addEdge(newEdge, prevEdges));
|
||||
updateNodesOnEdgeChange(newEdge, 'add');
|
||||
}
|
||||
});
|
||||
updateNodesOnEdgeChange(newEdge, 'add');
|
||||
return newEdges;
|
||||
});
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === connection.target || node.id === connection.source) {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
connections: [
|
||||
...node.data.connections,
|
||||
{
|
||||
source: connection.source,
|
||||
sourceHandle: connection.sourceHandle,
|
||||
target: connection.target,
|
||||
targetHandle: connection.targetHandle,
|
||||
} as { source: string; sourceHandle: string; target: string; targetHandle: string },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
})
|
||||
);
|
||||
clearNodesStatusAndOutput(); // Clear status and output on connection change
|
||||
},
|
||||
[nodes]
|
||||
);
|
||||
}
|
||||
|
||||
const onEdgesDelete = useCallback(
|
||||
(edgesToDelete: Edge<CustomEdgeData>[]) => {
|
||||
@@ -279,25 +187,24 @@ const FlowEditor: React.FC<{
|
||||
(edge) =>
|
||||
edge.source === conn.source &&
|
||||
edge.target === conn.target &&
|
||||
edge.sourceHandle === edge.sourceHandle &&
|
||||
edge.targetHandle === edge.targetHandle
|
||||
edge.sourceHandle === conn.sourceHandle &&
|
||||
edge.targetHandle === conn.targetHandle
|
||||
)
|
||||
),
|
||||
},
|
||||
}))
|
||||
);
|
||||
clearNodesStatusAndOutput(); // Clear status and output on edge deletion
|
||||
},
|
||||
[setNodes, clearNodesStatusAndOutput]
|
||||
[setNodes]
|
||||
);
|
||||
|
||||
const addNode = useCallback((blockId: string, nodeType: string) => {
|
||||
const addNode = (blockId: string, nodeType: string) => {
|
||||
const nodeSchema = availableNodes.find(node => node.id === blockId);
|
||||
if (!nodeSchema) {
|
||||
console.error(`Schema not found for block ID: ${blockId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const newNode: Node<CustomNodeData> = {
|
||||
id: nodeId.toString(),
|
||||
type: 'custom',
|
||||
@@ -308,46 +215,21 @@ const FlowEditor: React.FC<{
|
||||
inputSchema: nodeSchema.inputSchema,
|
||||
outputSchema: nodeSchema.outputSchema,
|
||||
hardcodedValues: {},
|
||||
setHardcodedValues: (values) => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) =>
|
||||
node.id === newNode.id ? { ...node, data: { ...node.data, hardcodedValues: values } } : node
|
||||
)
|
||||
);
|
||||
setHardcodedValues: (values: { [key: string]: any }) => {
|
||||
setNodes((nds) => nds.map((node) =>
|
||||
node.id === newNode.id
|
||||
? { ...node, data: { ...node.data, hardcodedValues: values } }
|
||||
: node
|
||||
));
|
||||
},
|
||||
connections: [],
|
||||
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
|
||||
|
||||
history.push({
|
||||
type: 'ADD_NODE',
|
||||
payload: newNode,
|
||||
undo: () => setNodes((nds) => nds.filter(node => node.id !== newNode.id)),
|
||||
redo: () => setNodes((nds) => [...nds, newNode])
|
||||
});
|
||||
}, [nodeId, availableNodes]);
|
||||
|
||||
|
||||
const handleUndo = () => {
|
||||
history.undo();
|
||||
};
|
||||
|
||||
const handleRedo = () => {
|
||||
history.redo();
|
||||
};
|
||||
|
||||
function loadGraph(graph: Graph) {
|
||||
@@ -362,7 +244,6 @@ const FlowEditor: React.FC<{
|
||||
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}`,
|
||||
@@ -384,14 +265,6 @@ const FlowEditor: React.FC<{
|
||||
targetHandle: link.sink_name,
|
||||
})),
|
||||
isOutputOpen: false,
|
||||
setIsAnyModalOpen: setIsAnyModalOpen, // Pass setIsAnyModalOpen function
|
||||
setErrors: (errors: { [key: string]: string | null }) => {
|
||||
setNodes((nds) => nds.map((node) =>
|
||||
node.id === newNode.id
|
||||
? { ...node, data: { ...node.data, errors } }
|
||||
: node
|
||||
));
|
||||
}
|
||||
},
|
||||
};
|
||||
return newNode;
|
||||
@@ -422,18 +295,13 @@ const FlowEditor: React.FC<{
|
||||
return {};
|
||||
}
|
||||
|
||||
const getNestedData = (
|
||||
schema: BlockIOSchema, values: { [key: string]: any }
|
||||
): { [key: string]: any } => {
|
||||
const getNestedData = (schema: ObjectSchema, values: { [key: string]: any }): { [key: string]: any } => {
|
||||
let inputData: { [key: string]: any } = {};
|
||||
|
||||
if ("properties" in schema) {
|
||||
if (schema.properties) {
|
||||
Object.keys(schema.properties).forEach((key) => {
|
||||
if (values[key] !== undefined) {
|
||||
if (
|
||||
"properties" in schema.properties[key]
|
||||
|| "additionalProperties" in schema.properties[key]
|
||||
) {
|
||||
if (schema.properties[key].type === 'object') {
|
||||
inputData[key] = getNestedData(schema.properties[key], values[key]);
|
||||
} else {
|
||||
inputData[key] = values[key];
|
||||
@@ -442,7 +310,7 @@ const FlowEditor: React.FC<{
|
||||
});
|
||||
}
|
||||
|
||||
if ("additionalProperties" in schema) {
|
||||
if (schema.additionalProperties) {
|
||||
inputData = { ...inputData, ...values };
|
||||
}
|
||||
|
||||
@@ -455,13 +323,12 @@ const FlowEditor: React.FC<{
|
||||
return inputData;
|
||||
};
|
||||
|
||||
async function saveAgent(asTemplate: boolean = false) {
|
||||
async function saveAgent (asTemplate: boolean = false) {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => ({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
hardcodedValues: removeEmptyStringsAndNulls(node.data.hardcodedValues),
|
||||
status: undefined,
|
||||
},
|
||||
}))
|
||||
@@ -496,10 +363,6 @@ const FlowEditor: React.FC<{
|
||||
input_default: inputDefault,
|
||||
input_nodes: inputNodes,
|
||||
output_nodes: outputNodes,
|
||||
data: {
|
||||
...node.data,
|
||||
hardcodedValues: removeEmptyStringsAndNulls(node.data.hardcodedValues),
|
||||
},
|
||||
metadata: { position: node.position }
|
||||
};
|
||||
});
|
||||
@@ -528,7 +391,7 @@ const FlowEditor: React.FC<{
|
||||
|
||||
const newSavedAgent = savedAgent
|
||||
? await (savedAgent.is_template
|
||||
? api.updateTemplate(savedAgent.id, payload)
|
||||
? api.updateTemplate(savedAgent.id, payload)
|
||||
: api.updateGraph(savedAgent.id, payload))
|
||||
: await (asTemplate
|
||||
? api.createTemplate(payload)
|
||||
@@ -559,41 +422,6 @@ const FlowEditor: React.FC<{
|
||||
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();
|
||||
@@ -602,11 +430,6 @@ const FlowEditor: React.FC<{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateNodes()) {
|
||||
console.error('Validation failed; aborting run');
|
||||
return;
|
||||
}
|
||||
|
||||
api.subscribeToExecution(newAgentId);
|
||||
api.runGraph(newAgentId);
|
||||
|
||||
@@ -615,7 +438,9 @@ const FlowEditor: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
const updateNodesWithExecutionData = (executionData: NodeExecutionResult[]) => {
|
||||
|
||||
|
||||
const updateNodesWithExecutionData = (executionData: any[]) => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
const nodeExecution = executionData.find((exec) => exec.node_id === node.data.backend_id);
|
||||
@@ -635,9 +460,9 @@ const FlowEditor: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const handleKeyDown = useCallback((event: KeyboardEvent) => {
|
||||
if (isAnyModalOpen) return; // Prevent copy/paste if any modal is open
|
||||
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
|
||||
|
||||
const handleKeyDown = useCallback((event: KeyboardEvent) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if (event.key === 'c' || event.key === 'C') {
|
||||
// Copy selected nodes
|
||||
@@ -690,7 +515,7 @@ const FlowEditor: React.FC<{
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [nodes, edges, copiedNodes, copiedEdges, nodeId, isAnyModalOpen]);
|
||||
}, [nodes, edges, copiedNodes, copiedEdges, nodeId]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
@@ -699,55 +524,59 @@ const FlowEditor: React.FC<{
|
||||
};
|
||||
}, [handleKeyDown]);
|
||||
|
||||
const onNodesDelete = useCallback(() => {
|
||||
clearNodesStatusAndOutput();
|
||||
}, [clearNodesStatusAndOutput]);
|
||||
|
||||
const editorControls: Control[] = [
|
||||
{
|
||||
label: 'Undo',
|
||||
icon: <Undo2 />,
|
||||
onClick: handleUndo,
|
||||
},
|
||||
{
|
||||
label: 'Redo',
|
||||
icon: <Redo2 />,
|
||||
onClick: handleRedo,
|
||||
},
|
||||
{
|
||||
label: 'Run',
|
||||
icon: <Play />,
|
||||
onClick: runAgent,
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleSidebar}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: isSidebarOpen ? '350px' : '10px',
|
||||
zIndex: 10000,
|
||||
backgroundColor: 'black',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{isSidebarOpen ? <ChevronLeft className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Sidebar isOpen={isSidebarOpen} availableNodes={availableNodes} addNode={addNode} />
|
||||
<ReactFlow
|
||||
nodes={nodes.map(node => ({ ...node, data: { ...node.data, setIsAnyModalOpen } }))}
|
||||
edges={edges.map(edge => ({...edge, data: { ...edge.data, clearNodesStatusAndOutput } }))}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
connectionLineComponent={ConnectionLine}
|
||||
onNodesDelete={onNodesDelete}
|
||||
onEdgesDelete={onEdgesDelete}
|
||||
deleteKeyCode={["Backspace", "Delete"]}
|
||||
onNodeDragStart={onNodesChangeStart}
|
||||
onNodeDragStop={onNodesChangeEnd}
|
||||
>
|
||||
<div className={"flex flex-row absolute z-10 gap-2"}>
|
||||
<ControlPanel controls={editorControls} >
|
||||
<BlocksControl blocks={availableNodes} addBlock={addNode} />
|
||||
<SaveControl
|
||||
agentMeta={savedAgent}
|
||||
onSave={saveAgent}
|
||||
onDescriptionChange={setAgentDescription}
|
||||
onNameChange={setAgentName}
|
||||
/>
|
||||
</ControlPanel>
|
||||
<div style={{ position: 'absolute', right: 10, zIndex: 4 }}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Agent Name"
|
||||
value={agentName}
|
||||
onChange={(e) => setAgentName(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Agent Description"
|
||||
value={agentDescription}
|
||||
onChange={(e) => setAgentDescription(e.target.value)}
|
||||
/>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}> {/* Added gap for spacing */}
|
||||
<Button onClick={() => saveAgent(savedAgent?.is_template)}>
|
||||
Save {savedAgent?.is_template ? "Template" : "Agent"}
|
||||
</Button>
|
||||
{!savedAgent?.is_template &&
|
||||
<Button onClick={runAgent}>Save & Run Agent</Button>
|
||||
}
|
||||
{!savedAgent &&
|
||||
<Button onClick={() => saveAgent(true)}>Save as Template</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent, DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import Link from "next/link";
|
||||
import { CircleUser, Menu, SquareActivity, Workflow } from "lucide-react";
|
||||
import { Button, buttonVariants } 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";
|
||||
import getServerUser from "@/hooks/getServerUser";
|
||||
import ProfileDropdown from "./ProfileDropdown";
|
||||
|
||||
export async function NavBar() {
|
||||
const isAvailable = Boolean(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
);
|
||||
const { user } = await getServerUser();
|
||||
|
||||
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 "
|
||||
>
|
||||
<SquareActivity className="size-6" /> Monitor
|
||||
</Link>
|
||||
<Link
|
||||
href="/build"
|
||||
className="text-muted-foreground hover:text-foreground flex flex-row gap-2"
|
||||
>
|
||||
<Workflow 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"
|
||||
>
|
||||
<SquareActivity className="size-4" /> Monitor
|
||||
</Link>
|
||||
<Link
|
||||
href="/build"
|
||||
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
|
||||
>
|
||||
<Workflow 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">
|
||||
{isAvailable && !user &&
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
|
||||
>
|
||||
Log In<CircleUser className="size-5" />
|
||||
</Link>}
|
||||
{isAvailable && user && <ProfileDropdown />}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BlockIOSchema } from "@/lib/autogpt-server-api/types";
|
||||
import { BlockSchema } from "@/lib/types";
|
||||
import { beautifyString, getTypeBgColor, getTypeTextColor } from "@/lib/utils";
|
||||
import { FC } from "react";
|
||||
import { Handle, Position } from "reactflow";
|
||||
@@ -6,13 +6,12 @@ import SchemaTooltip from "./SchemaTooltip";
|
||||
|
||||
type HandleProps = {
|
||||
keyName: string,
|
||||
schema: BlockIOSchema,
|
||||
schema: BlockSchema,
|
||||
isConnected: boolean,
|
||||
isRequired?: boolean,
|
||||
side: 'left' | 'right'
|
||||
}
|
||||
|
||||
const NodeHandle: FC<HandleProps> = ({ keyName, schema, isConnected, isRequired, side }) => {
|
||||
const NodeHandle: FC<HandleProps> = ({ keyName, isConnected, schema, side }) => {
|
||||
|
||||
const typeName: Record<string, string> = {
|
||||
string: 'text',
|
||||
@@ -27,9 +26,7 @@ const NodeHandle: FC<HandleProps> = ({ keyName, schema, isConnected, isRequired,
|
||||
|
||||
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="text-m text-gray-900 -mb-1 green">{schema.title || beautifyString(keyName)}</span>
|
||||
<span className={typeClass}>{typeName[schema.type]}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,146 +1,136 @@
|
||||
import { Cross2Icon, PlusIcon } from "@radix-ui/react-icons";
|
||||
import { beautifyString } from "@/lib/utils";
|
||||
import { BlockIOSchema } from "@/lib/autogpt-server-api/types";
|
||||
import { FC, useState } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
|
||||
type BlockInputFieldProps = {
|
||||
keyName: string
|
||||
schema: BlockIOSchema
|
||||
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
|
||||
errors: { [key: string]: string | null }
|
||||
}
|
||||
|
||||
const NodeInputField: FC<BlockInputFieldProps> = ({
|
||||
keyName: key,
|
||||
schema,
|
||||
parentKey = '',
|
||||
value,
|
||||
handleInputClick,
|
||||
handleInputChange,
|
||||
errors
|
||||
}) => {
|
||||
const fullKey = parentKey ? `${parentKey}.${key}` : key;
|
||||
const error = typeof errors === 'string' ? errors : errors?.[key] ?? "";
|
||||
const displayKey = schema.title || beautifyString(key);
|
||||
const NodeInputField: FC<BlockInputFieldProps> =
|
||||
({ keyName: key, schema, parentKey = '', value, handleInputClick, handleInputChange, errors }) => {
|
||||
const fullKey = parentKey ? `${parentKey}.${key}` : key;
|
||||
const error = errors[fullKey];
|
||||
const displayKey = schema.title || beautifyString(key);
|
||||
|
||||
const [keyValuePairs, _setKeyValuePairs] = useState<{ key: string, value: string }[]>(
|
||||
"additionalProperties" in schema && value
|
||||
? Object.entries(value).map(([key, value]) => ({ key: key, value: value }))
|
||||
: []
|
||||
);
|
||||
|
||||
function setKeyValuePairs(newKVPairs: typeof keyValuePairs): void {
|
||||
_setKeyValuePairs(newKVPairs);
|
||||
handleInputChange(
|
||||
fullKey,
|
||||
newKVPairs.reduce((obj, { key, value }) => ({ ...obj, [key]: value }), {})
|
||||
const [keyValuePairs, _setKeyValuePairs] = useState<{ key: string, value: string }[]>(
|
||||
"additionalProperties" in schema && value
|
||||
? Object.entries(value).map(([key, value]) => ({ key: key, value: value}))
|
||||
: []
|
||||
);
|
||||
}
|
||||
|
||||
const renderClickableInput = (value: string | null = null, placeholder: string = "", secret: boolean = false) => {
|
||||
const className = `clickable-input ${error ? 'border-error' : ''}`;
|
||||
function setKeyValuePairs(newKVPairs: typeof keyValuePairs): void {
|
||||
_setKeyValuePairs(newKVPairs);
|
||||
handleInputChange(
|
||||
fullKey,
|
||||
newKVPairs.reduce((obj, {key, value}) => ({ ...obj, [key]: value }), {})
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
const renderClickableInput = (value: string | null = null, placeholder: string = "", secret: boolean = false) => {
|
||||
// if secret is true, then the input field will be a password field if the value is not null
|
||||
return secret ? (
|
||||
<div className="clickable-input" onClick={() => handleInputClick(fullKey)}>
|
||||
{value ? <i className="text-gray-500">********</i> : <i className="text-gray-500">{placeholder}</i>}
|
||||
</div>
|
||||
) : (
|
||||
<div className="clickable-input" onClick={() => handleInputClick(fullKey)}>
|
||||
{value || <i className="text-gray-500">{placeholder}</i>}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
if ("properties" in schema) {
|
||||
return (
|
||||
<div key={fullKey} className="object-input">
|
||||
<strong>{displayKey}:</strong>
|
||||
{Object.entries(schema.properties).map(([propKey, propSchema]) => (
|
||||
<div key={`${fullKey}.${propKey}`} className="nested-input">
|
||||
<NodeInputField
|
||||
keyName={propKey}
|
||||
schema={propSchema}
|
||||
parentKey={fullKey}
|
||||
value={(value as { [key: string]: string })[propKey]}
|
||||
handleInputClick={handleInputClick}
|
||||
handleInputChange={handleInputChange}
|
||||
errors={errors}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.type === 'object' && schema.additionalProperties) {
|
||||
return (
|
||||
<div key={fullKey}>
|
||||
<div>
|
||||
{keyValuePairs.map(({ key, value }, index) => (
|
||||
<div key={index} className="flex items-center w-[325px] space-x-2 mb-2">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Key"
|
||||
value={key}
|
||||
onChange={(e) => setKeyValuePairs(
|
||||
keyValuePairs.toSpliced(index, 1, {
|
||||
key: e.target.value, value: value
|
||||
})
|
||||
)}
|
||||
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}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Value"
|
||||
value={value}
|
||||
onChange={(e) => setKeyValuePairs(
|
||||
keyValuePairs.toSpliced(index, 1, {
|
||||
key: key, value: e.target.value
|
||||
})
|
||||
)}
|
||||
/>
|
||||
<Button variant="ghost" className="px-2"
|
||||
onClick={() => setKeyValuePairs(keyValuePairs.toSpliced(index, 1))}
|
||||
>
|
||||
<Cross2Icon />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button className="w-full"
|
||||
onClick={() => setKeyValuePairs(
|
||||
keyValuePairs.concat({ key: "", value: "" })
|
||||
)}
|
||||
>
|
||||
<PlusIcon className="mr-2" /> Add Property
|
||||
</Button>
|
||||
</div>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ("anyOf" in schema) {
|
||||
const types = schema.anyOf.map(s => "type" in s ? s.type : undefined);
|
||||
if (types.includes('string') && types.includes('null')) {
|
||||
if (schema.type === 'object' && schema.additionalProperties) {
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
{renderClickableInput(value as string, schema.placeholder || `Enter ${displayKey} (optional)`)}
|
||||
<div key={fullKey}>
|
||||
<div>
|
||||
{keyValuePairs.map(({ key, value }, index) => (
|
||||
<div key={index} className="flex items-center w-[325px] space-x-2 mb-2">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Key"
|
||||
value={key}
|
||||
onChange={(e) => setKeyValuePairs(
|
||||
keyValuePairs.toSpliced(index, 1, {
|
||||
key: e.target.value, value: value
|
||||
})
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Value"
|
||||
value={value}
|
||||
onChange={(e) => setKeyValuePairs(
|
||||
keyValuePairs.toSpliced(index, 1, {
|
||||
key: key, value: e.target.value
|
||||
})
|
||||
)}
|
||||
/>
|
||||
<Button variant="ghost" className="px-2"
|
||||
onClick={() => setKeyValuePairs(keyValuePairs.toSpliced(index, 1))}
|
||||
>
|
||||
<Cross2Icon />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button className="w-full"
|
||||
onClick={() => setKeyValuePairs(
|
||||
keyValuePairs.concat({ key: "", value: "" })
|
||||
)}
|
||||
>
|
||||
<PlusIcon className="mr-2" /> Add Property
|
||||
</Button>
|
||||
</div>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ("allOf" in schema) {
|
||||
return (
|
||||
<div key={fullKey} className="object-input">
|
||||
<strong>{displayKey}:</strong>
|
||||
{"properties" in schema.allOf[0] &&
|
||||
Object.entries(schema.allOf[0].properties).map(([propKey, propSchema]) => (
|
||||
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}
|
||||
@@ -153,16 +143,15 @@ const NodeInputField: FC<BlockInputFieldProps> = ({
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ("oneOf" in schema) {
|
||||
return (
|
||||
<div key={fullKey} className="object-input">
|
||||
<strong>{displayKey}:</strong>
|
||||
{"properties" in schema.oneOf[0] &&
|
||||
Object.entries(schema.oneOf[0].properties).map(([propKey, propSchema]) => (
|
||||
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}
|
||||
@@ -175,120 +164,117 @@ const NodeInputField: FC<BlockInputFieldProps> = ({
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!("type" in schema)) {
|
||||
console.warn(`Schema for input ${key} does not specify a type:`, schema);
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
{renderClickableInput(value as string, schema.placeholder || `Enter ${beautifyString(displayKey)} (Complex)`)}
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
switch (schema.type) {
|
||||
case 'string':
|
||||
if (schema.enum) {
|
||||
|
||||
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 as string || ''}
|
||||
onChange={(e) => handleInputChange(fullKey, e.target.value)}
|
||||
value={value === undefined ? '' : value.toString()}
|
||||
onChange={(e) => handleInputChange(fullKey, e.target.value === 'true')}
|
||||
className="select-input"
|
||||
>
|
||||
<option value="">Select {displayKey}</option>
|
||||
{schema.enum.map((option: string) => (
|
||||
<option key={option} value={option}>
|
||||
{beautifyString(option)}
|
||||
</option>
|
||||
))}
|
||||
<option value="true">True</option>
|
||||
<option value="false">False</option>
|
||||
</select>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.secret) {
|
||||
case 'number':
|
||||
case 'integer':
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
{renderClickableInput(value as string, schema.placeholder || `Enter ${displayKey}`, true)}
|
||||
<input
|
||||
type="number"
|
||||
value={value as string || ''}
|
||||
onChange={(e) => handleInputChange(fullKey, parseFloat(e.target.value))}
|
||||
className="number-input"
|
||||
/>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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> || [];
|
||||
case 'array':
|
||||
if (schema.items) {
|
||||
const arrayValues = value as Array<string> || [];
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
{arrayValues.map((item: string, index: number) => (
|
||||
<div key={`${fullKey}.${index}`} className="flex items-center space-x-2 mb-2">
|
||||
<NodeInputField
|
||||
value={item}
|
||||
keyName={index.toString()}
|
||||
parentKey={fullKey}
|
||||
handleInputChange={handleInputChange}
|
||||
handleInputClick={handleInputClick}
|
||||
schema={schema.items}
|
||||
errors={errors}
|
||||
/>
|
||||
<Button variant="ghost" className="px-2" onClick={
|
||||
() => handleInputChange(fullKey, arrayValues.toSpliced(index, 1))
|
||||
}>
|
||||
<Cross2Icon />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button onClick={
|
||||
() => handleInputChange(fullKey, [...arrayValues, ''])
|
||||
}>
|
||||
<PlusIcon className="mr-2"/> Add Item
|
||||
</Button>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
default:
|
||||
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>}
|
||||
{renderClickableInput(value as string, schema.placeholder || `Enter ${beautifyString(displayKey)} (Complex)`)}
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
default:
|
||||
console.warn(`Schema for input ${key} specifies unknown type:`, schema);
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
{renderClickableInput(value as string, schema.placeholder || `Enter ${beautifyString(displayKey)} (Complex)`)}
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default NodeInputField;
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { forwardRef, useState } from "react"
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input, InputProps } from "@/components/ui/input"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const PasswordInput = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const disabled = props.value === "" || props.value === undefined || props.disabled
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
className={cn("hide-password-toggle pr-10", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute top-0 right-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{showPassword && !disabled ? (
|
||||
<EyeIcon
|
||||
className="w-4 h-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<EyeOffIcon
|
||||
className="w-4 h-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<span className="sr-only">
|
||||
{showPassword ? "Hide password" : "Show password"}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
{/* hides browsers password toggles */}
|
||||
<style>{`
|
||||
.hide-password-toggle::-ms-reveal,
|
||||
.hide-password-toggle::-ms-clear {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
PasswordInput.displayName = "PasswordInput"
|
||||
|
||||
export { PasswordInput }
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "./ui/button";
|
||||
import { useSupabase } from "./SupabaseProvider";
|
||||
import { useRouter } from "next/navigation";
|
||||
import useUser from "@/hooks/useUser";
|
||||
|
||||
const ProfileDropdown = () => {
|
||||
const { supabase } = useSupabase();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<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 onClick={() => router.push('profile')}>Profile</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => supabase?.auth.signOut()}>Log out</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileDropdown;
|
||||
@@ -4,11 +4,11 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { BlockIOSchema } from "@/lib/autogpt-server-api/types";
|
||||
import { BlockSchema } from "@/lib/types";
|
||||
import { Info } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
const SchemaTooltip: React.FC<{ schema: BlockIOSchema }> = ({ schema }) => {
|
||||
const SchemaTooltip: React.FC<{ schema: BlockSchema }> = ({ schema }) => {
|
||||
if (!schema.description) return null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
type SupabaseContextType = {
|
||||
supabase: SupabaseClient | null;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
const Context = createContext<SupabaseContextType | undefined>(undefined);
|
||||
|
||||
export default function SupabaseProvider({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [supabase, setSupabase] = useState<SupabaseClient | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const initializeSupabase = async () => {
|
||||
setIsLoading(true);
|
||||
const client = createClient();
|
||||
setSupabase(client);
|
||||
setIsLoading(false);
|
||||
|
||||
if (client) {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = client.auth.onAuthStateChange(() => {
|
||||
router.refresh();
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
initializeSupabase();
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<Context.Provider value={{ supabase, isLoading }}>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useSupabase = () => {
|
||||
const context = useContext(Context);
|
||||
if (context === undefined) {
|
||||
throw new Error('useSupabase must be used inside SupabaseProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
padding: 15px;
|
||||
border: 3px solid #4b5563;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
background: #ffffff; /* White background */
|
||||
color: #000000;
|
||||
width: 500px;
|
||||
box-sizing: border-box;
|
||||
@@ -16,60 +16,6 @@
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.custom-node .mb-2 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 40px;
|
||||
/* Increased to accommodate larger buttons */
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.custom-node .mb-2 .text-lg {
|
||||
flex-grow: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.node-actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.node-action-button {
|
||||
width: 32px;
|
||||
/* Increased size */
|
||||
height: 32px;
|
||||
/* Increased size */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f3f4f6;
|
||||
/* Light gray background */
|
||||
border: 1px solid #d1d5db;
|
||||
/* Light border */
|
||||
border-radius: 6px;
|
||||
color: #4b5563;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.node-action-button:hover {
|
||||
background-color: #e5e7eb;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.node-action-button:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
.node-action-button svg {
|
||||
width: 18px;
|
||||
/* Increased icon size */
|
||||
height: 18px;
|
||||
/* Increased icon size */
|
||||
}
|
||||
/* Existing styles */
|
||||
.handle-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
@@ -108,10 +54,6 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.border-error {
|
||||
border: 1px solid #d9534f;
|
||||
}
|
||||
|
||||
.clickable-input span {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
@@ -140,6 +82,7 @@
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #000;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
}
|
||||
@@ -229,14 +172,6 @@
|
||||
border-color: #c0392b; /* Red border for failed nodes */
|
||||
}
|
||||
|
||||
.incomplete {
|
||||
border-color: #9f14ab; /* Pink border for incomplete nodes */
|
||||
}
|
||||
|
||||
.queued {
|
||||
border-color: #25e6e6; /* Cyanic border for failed nodes */
|
||||
}
|
||||
|
||||
.custom-switch {
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import {Card, CardContent} from "@/components/ui/card";
|
||||
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* Represents a control element for the ControlPanel Component.
|
||||
* @type {Object} Control
|
||||
* @property {React.ReactNode} icon - The icon of the control from lucide-react https://lucide.dev/icons/
|
||||
* @property {string} label - The label of the control, to be leveraged by ToolTip.
|
||||
* @property {onclick} onClick - The function to be executed when the control is clicked.
|
||||
*/
|
||||
export type Control = {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
interface ControlPanelProps {
|
||||
controls: Control[];
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* ControlPanel component displays a panel with controls as icons with the ability to take in children.
|
||||
* @param {Object} ControlPanelProps - The properties of the control panel component.
|
||||
* @param {Array} ControlPanelProps.controls - An array of control objects representing actions to be preformed.
|
||||
* @param {Array} ControlPanelProps.children - The child components of the control panel.
|
||||
* @returns The rendered control panel component.
|
||||
*/
|
||||
export const ControlPanel= ( {controls, children}: ControlPanelProps) => {
|
||||
return (
|
||||
<aside className="hidden w-14 flex-col sm:flex">
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col items-center gap-4 px-2 sm:py-5 rounded-radius">
|
||||
{controls.map((control, index) => (
|
||||
<Tooltip key={index} delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => control.onClick()}
|
||||
>
|
||||
{control.icon}
|
||||
<span className="sr-only">{control.label}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{control.label}</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
<Separator />
|
||||
{children}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
export default ControlPanel;
|
||||
@@ -1,83 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ToyBrick } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { beautifyString } from "@/lib/utils";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Block } from '@/lib/autogpt-server-api';
|
||||
import { PlusIcon } from "@radix-ui/react-icons";
|
||||
|
||||
interface BlocksControlProps {
|
||||
blocks: Block[];
|
||||
addBlock: (id: string, name: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A React functional component that displays a control for managing blocks.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} BlocksControlProps - The properties for the BlocksControl component.
|
||||
* @param {Block[]} BlocksControlProps.blocks - An array of blocks to be displayed and filtered.
|
||||
* @param {(id: string, name: string) => void} BlocksControlProps.addBlock - A function to call when a block is added.
|
||||
* @returns The rendered BlocksControl component.
|
||||
*/
|
||||
export const BlocksControl: React.FC<BlocksControlProps> = ({ blocks, addBlock }) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const filteredBlocks = blocks.filter((block: Block) =>
|
||||
block.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger className="hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50 dark:text-white">
|
||||
<ToyBrick className="size-4"/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="right" sideOffset={15} align="start" className="w-80 p-0">
|
||||
<Card className="border-none shadow-none">
|
||||
<CardHeader className="p-4">
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<Label htmlFor="search-blocks">Blocks</Label>
|
||||
</div>
|
||||
<Input
|
||||
id="search-blocks"
|
||||
type="text"
|
||||
placeholder="Search blocks..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="p-1">
|
||||
<ScrollArea className="h-[60vh]">
|
||||
{filteredBlocks.map((block) => (
|
||||
<Card
|
||||
key={block.id}
|
||||
className="m-2"
|
||||
>
|
||||
<div className="flex items-center justify-between m-3">
|
||||
<div className="flex-1 min-w-0 mr-2">
|
||||
<span className="font-medium truncate block">{beautifyString(block.name)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => addBlock(block.id, block.name)}
|
||||
aria-label="Add block"
|
||||
>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
import {Card, CardContent} from "@/components/ui/card";
|
||||
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* Represents a control element for the ControlPanel Component.
|
||||
* @type {Object} Control
|
||||
* @property {React.ReactNode} icon - The icon of the control from lucide-react https://lucide.dev/icons/
|
||||
* @property {string} label - The label of the control, to be leveraged by ToolTip.
|
||||
* @property {onclick} onClick - The function to be executed when the control is clicked.
|
||||
*/
|
||||
export type Control = {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
interface ControlPanelProps {
|
||||
controls: Control[];
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* ControlPanel component displays a panel with controls as icons with the ability to take in children.
|
||||
* @param {Object} ControlPanelProps - The properties of the control panel component.
|
||||
* @param {Array} ControlPanelProps.controls - An array of control objects representing actions to be preformed.
|
||||
* @param {Array} ControlPanelProps.children - The child components of the control panel.
|
||||
* @returns The rendered control panel component.
|
||||
*/
|
||||
export const ControlPanel= ( {controls, children}: ControlPanelProps) => {
|
||||
return (
|
||||
<aside className="hidden w-14 flex-col sm:flex">
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col items-center gap-4 px-2 sm:py-5 rounded-radius">
|
||||
{children}
|
||||
<Separator />
|
||||
{controls.map((control, index) => (
|
||||
<Tooltip key={index} delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => control.onClick()}
|
||||
>
|
||||
{control.icon}
|
||||
<span className="sr-only">{control.label}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{control.label}</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
export default ControlPanel;
|
||||
@@ -1,99 +0,0 @@
|
||||
import React from "react";
|
||||
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
|
||||
import {Card, CardContent, CardFooter} from "@/components/ui/card";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {GraphMeta} from "@/lib/autogpt-server-api";
|
||||
import {Label} from "@/components/ui/label";
|
||||
import {Save} from "lucide-react";
|
||||
|
||||
interface SaveControlProps {
|
||||
agentMeta: GraphMeta | null;
|
||||
onSave: (isTemplate: boolean | undefined) => void;
|
||||
onNameChange: (name: string) => void;
|
||||
onDescriptionChange: (description: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A SaveControl component to be used within the ControlPanel. It allows the user to save the agent / template.
|
||||
* @param {Object} SaveControlProps - The properties of the SaveControl component.
|
||||
* @param {GraphMeta | null} SaveControlProps.agentMeta - The agent's metadata, or null if creating a new agent.
|
||||
* @param {(isTemplate: boolean | undefined) => void} SaveControlProps.onSave - Function to save the agent or template.
|
||||
* @param {(name: string) => void} SaveControlProps.onNameChange - Function to handle name changes.
|
||||
* @param {(description: string) => void} SaveControlProps.onDescriptionChange - Function to handle description changes.
|
||||
* @returns The SaveControl component.
|
||||
*/
|
||||
export const SaveControl= ({
|
||||
agentMeta,
|
||||
onSave,
|
||||
onNameChange,
|
||||
onDescriptionChange
|
||||
}: SaveControlProps) => {
|
||||
|
||||
/**
|
||||
* Note for improvement:
|
||||
* At the moment we are leveraging onDescriptionChange and onNameChange to handle the changes in the description and name of the agent.
|
||||
* We should migrate this to be handled with form controls and a form library.
|
||||
*/
|
||||
|
||||
// Determines if we're saving a template or an agent
|
||||
let isTemplate = agentMeta?.is_template ? true : undefined;
|
||||
const handleSave = () => {
|
||||
onSave(isTemplate);
|
||||
};
|
||||
|
||||
const getType = () => {
|
||||
return agentMeta?.is_template ? 'template' : 'agent';
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover >
|
||||
<PopoverTrigger
|
||||
className="hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50 dark:text-white"
|
||||
>
|
||||
<Save className="size-4"/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="right" sideOffset={15} align="start">
|
||||
<Card className="border-none shadow-none">
|
||||
<CardContent className="p-4">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="name">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="Enter your agent name"
|
||||
className="col-span-3"
|
||||
defaultValue={agentMeta?.name || ''}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
/>
|
||||
<Label htmlFor="description">
|
||||
Description
|
||||
</Label>
|
||||
<Input
|
||||
id="description"
|
||||
placeholder="Your agent description"
|
||||
className="col-span-3"
|
||||
defaultValue={agentMeta?.description || ''}
|
||||
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col items-stretch gap-2 ">
|
||||
<Button className="w-full" onClick={handleSave}>
|
||||
Save {getType()}
|
||||
</Button>
|
||||
{!agentMeta && (
|
||||
<Button variant="secondary" className="w-full" onClick={() => {
|
||||
isTemplate = true;
|
||||
handleSave();
|
||||
}}>
|
||||
Save as Template
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
/* 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 {
|
||||
@@ -125,6 +130,7 @@ input::placeholder, textarea::placeholder {
|
||||
.flow-container {
|
||||
width: 100%;
|
||||
height: 600px; /* Adjust this height as needed */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flow-wrapper {
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
// history.ts
|
||||
import { CustomNodeData } from './CustomNode';
|
||||
import { CustomEdgeData } from './CustomEdge';
|
||||
import { Edge } from 'reactflow';
|
||||
|
||||
type ActionType =
|
||||
| 'ADD_NODE'
|
||||
| 'DELETE_NODE'
|
||||
| 'ADD_EDGE'
|
||||
| 'DELETE_EDGE'
|
||||
| 'UPDATE_NODE'
|
||||
| 'MOVE_NODE'
|
||||
| 'UPDATE_INPUT'
|
||||
| 'UPDATE_NODE_POSITION';
|
||||
|
||||
type AddNodePayload = { node: CustomNodeData };
|
||||
type DeleteNodePayload = { nodeId: string };
|
||||
type AddEdgePayload = { edge: Edge<CustomEdgeData> };
|
||||
type DeleteEdgePayload = { edgeId: string };
|
||||
type UpdateNodePayload = { nodeId: string; newData: Partial<CustomNodeData> };
|
||||
type MoveNodePayload = { nodeId: string; position: { x: number; y: number } };
|
||||
type UpdateInputPayload = { nodeId: string; oldValues: { [key: string]: any }; newValues: { [key: string]: any } };
|
||||
type UpdateNodePositionPayload = { nodeId: string; oldPosition: { x: number; y: number }; newPosition: { x: number; y: number } };
|
||||
|
||||
type ActionPayload =
|
||||
| AddNodePayload
|
||||
| DeleteNodePayload
|
||||
| AddEdgePayload
|
||||
| DeleteEdgePayload
|
||||
| UpdateNodePayload
|
||||
| MoveNodePayload
|
||||
| UpdateInputPayload
|
||||
| UpdateNodePositionPayload;
|
||||
|
||||
type Action = {
|
||||
type: ActionType;
|
||||
payload: ActionPayload;
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
};
|
||||
|
||||
class History {
|
||||
private past: Action[] = [];
|
||||
private future: Action[] = [];
|
||||
|
||||
push(action: Action) {
|
||||
this.past.push(action);
|
||||
this.future = [];
|
||||
}
|
||||
|
||||
undo() {
|
||||
const action = this.past.pop();
|
||||
if (action) {
|
||||
action.undo();
|
||||
this.future.push(action);
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
const action = this.future.pop();
|
||||
if (action) {
|
||||
action.redo();
|
||||
this.past.push(action);
|
||||
}
|
||||
}
|
||||
|
||||
canUndo(): boolean {
|
||||
return this.past.length > 0;
|
||||
}
|
||||
|
||||
canRedo(): boolean {
|
||||
return this.future.length > 0;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.past = [];
|
||||
this.future = [];
|
||||
}
|
||||
|
||||
getHistoryState() {
|
||||
return {
|
||||
past: [...this.past],
|
||||
future: [...this.future],
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const history = new History();
|
||||
@@ -5,20 +5,20 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-neutral-300",
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-gray-300",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-neutral-900 text-neutral-50 shadow hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90",
|
||||
"bg-gray-900 text-gray-50 shadow hover:bg-gray-900/90 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-50/90",
|
||||
destructive:
|
||||
"bg-red-500 text-neutral-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90",
|
||||
"bg-red-500 text-gray-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/90",
|
||||
outline:
|
||||
"border border-neutral-200 bg-white shadow-sm hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
|
||||
"border border-gray-200 bg-white shadow-sm hover:bg-gray-100 hover:text-gray-900 dark:border-gray-800 dark:bg-gray-950 dark:hover:bg-gray-800 dark:hover:text-gray-50",
|
||||
secondary:
|
||||
"bg-neutral-100 text-neutral-900 shadow-sm hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80",
|
||||
ghost: "hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
|
||||
link: "text-neutral-900 underline-offset-4 hover:underline dark:text-neutral-50",
|
||||
"bg-gray-100 text-gray-900 shadow-sm hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/80",
|
||||
ghost: "hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50 dark:text-white",
|
||||
link: "text-gray-900 underline-offset-4 hover:underline dark:text-gray-50",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
@@ -1,48 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-neutral-200 dark:bg-neutral-800" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-neutral-200 dark:bg-neutral-800",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
@@ -1,143 +0,0 @@
|
||||
"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,
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { createServerClient } from "@/lib/supabase/server";
|
||||
|
||||
const getServerUser = async () => {
|
||||
const supabase = createServerClient();
|
||||
|
||||
if (!supabase) {
|
||||
return { user: null, error: 'Failed to create Supabase client' };
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
if (error) {
|
||||
return { user: null, error: error.message };
|
||||
}
|
||||
return { user: data.user, error: null };
|
||||
} catch (error) {
|
||||
return { user: null, error: (error as Error).message };
|
||||
}
|
||||
};
|
||||
|
||||
export default getServerUser;
|
||||
@@ -1,47 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { User, Session } from '@supabase/supabase-js';
|
||||
import { useSupabase } from '@/components/SupabaseProvider';
|
||||
|
||||
const useUser = () => {
|
||||
const { supabase, isLoading: isSupabaseLoading } = useSupabase();
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSupabaseLoading || !supabase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
setUser(user);
|
||||
setSession(session);
|
||||
} catch (e) {
|
||||
setError('Failed to fetch user data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUser();
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
setUser(session?.user ?? null);
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, [supabase, isSupabaseLoading]);
|
||||
|
||||
return { user, session, isLoading: isLoading || isSupabaseLoading, error };
|
||||
};
|
||||
|
||||
export default useUser;
|
||||
@@ -182,9 +182,7 @@ export default class AutoGPTServerAPI {
|
||||
}
|
||||
}
|
||||
|
||||
sendWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
|
||||
method: M, data: WebsocketMessageTypeMap[M]
|
||||
) {
|
||||
sendWebSocketMessage(method: string, data: any) {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(JSON.stringify({ method, data }));
|
||||
} else {
|
||||
@@ -192,9 +190,7 @@ export default class AutoGPTServerAPI {
|
||||
}
|
||||
}
|
||||
|
||||
onWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
|
||||
method: M, handler: (data: WebsocketMessageTypeMap[M]) => void
|
||||
) {
|
||||
onWebSocketMessage(method: string, handler: (data: any) => void) {
|
||||
this.messageHandlers[method] = handler;
|
||||
}
|
||||
|
||||
@@ -202,7 +198,7 @@ export default class AutoGPTServerAPI {
|
||||
this.sendWebSocketMessage('subscribe', { graph_id: graphId });
|
||||
}
|
||||
|
||||
runGraph(graphId: string, data: WebsocketMessageTypeMap["run_graph"]["data"] = {}) {
|
||||
runGraph(graphId: string, data: any = {}) {
|
||||
this.sendWebSocketMessage('run_graph', { graph_id: graphId, data });
|
||||
}
|
||||
}
|
||||
@@ -216,9 +212,3 @@ type GraphCreateRequestBody = {
|
||||
} | {
|
||||
graph: GraphCreatable;
|
||||
}
|
||||
|
||||
type WebsocketMessageTypeMap = {
|
||||
subscribe: { graph_id: string; };
|
||||
run_graph: { graph_id: string; data: { [key: string]: any }; };
|
||||
execution_event: NodeExecutionResult;
|
||||
}
|
||||
|
||||
@@ -3,56 +3,15 @@ export type Block = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: BlockIORootSchema;
|
||||
outputSchema: BlockIORootSchema;
|
||||
inputSchema: ObjectSchema;
|
||||
outputSchema: ObjectSchema;
|
||||
};
|
||||
|
||||
export type BlockIORootSchema = {
|
||||
type: "object";
|
||||
properties: { [key: string]: BlockIOSchema };
|
||||
required?: string[];
|
||||
export type ObjectSchema = {
|
||||
type: string;
|
||||
properties: { [key: string]: any };
|
||||
additionalProperties?: { type: string };
|
||||
}
|
||||
|
||||
export type BlockIOSchema = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
} & (BlockIOSimpleTypeSchema | BlockIOCombinedTypeSchema);
|
||||
|
||||
type BlockIOSimpleTypeSchema = {
|
||||
type: "object";
|
||||
properties: { [key: string]: BlockIOSchema };
|
||||
required?: string[];
|
||||
additionalProperties?: { type: string };
|
||||
} | {
|
||||
type: "array";
|
||||
items?: BlockIOSimpleTypeSchema;
|
||||
} | {
|
||||
type: "string";
|
||||
enum?: string[];
|
||||
secret?: true;
|
||||
default?: string;
|
||||
} | {
|
||||
type: "integer" | "number";
|
||||
default?: number;
|
||||
} | {
|
||||
type: "boolean";
|
||||
default?: boolean;
|
||||
} | {
|
||||
type: "null";
|
||||
};
|
||||
|
||||
// At the time of writing, combined schemas only occur on the first nested level in a
|
||||
// block schema. It is typed this way to make the use of these objects less tedious.
|
||||
type BlockIOCombinedTypeSchema = {
|
||||
allOf: [BlockIOSimpleTypeSchema];
|
||||
} | {
|
||||
anyOf: BlockIOSimpleTypeSchema[];
|
||||
default?: string | number | boolean | null;
|
||||
} | {
|
||||
oneOf: BlockIOSimpleTypeSchema[];
|
||||
default?: string | number | boolean | null;
|
||||
};
|
||||
|
||||
/* Mirror of autogpt_server/data/graph.py:Node */
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
|
||||
export function createClient() {
|
||||
try {
|
||||
return createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
)
|
||||
}
|
||||
catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
|
||||
export async function updateSession(request: NextRequest) {
|
||||
let supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
})
|
||||
|
||||
const isAvailable = Boolean(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
);
|
||||
|
||||
if (!isAvailable) {
|
||||
return supabaseResponse
|
||||
}
|
||||
|
||||
try {
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return request.cookies.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
|
||||
supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
})
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
supabaseResponse.cookies.set(name, value, options)
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// IMPORTANT: Avoid writing any logic between createServerClient and
|
||||
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
|
||||
// issues with users being randomly logged out.
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (
|
||||
!user &&
|
||||
!request.nextUrl.pathname.startsWith('/login') &&
|
||||
!request.nextUrl.pathname.startsWith('/auth')
|
||||
) {
|
||||
// no user, potentially respond by redirecting the user to the login page
|
||||
const url = request.nextUrl.clone()
|
||||
url.pathname = '/login'
|
||||
// return NextResponse.redirect(url)
|
||||
}
|
||||
|
||||
// IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
|
||||
// creating a new response object with NextResponse.next() make sure to:
|
||||
// 1. Pass the request in it, like so:
|
||||
// const myNewResponse = NextResponse.next({ request })
|
||||
// 2. Copy over the cookies, like so:
|
||||
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
|
||||
// 3. Change the myNewResponse object to fit your needs, but avoid changing
|
||||
// the cookies!
|
||||
// 4. Finally:
|
||||
// return myNewResponse
|
||||
// If this is not done, you may be causing the browser and server to go out
|
||||
// of sync and terminate the user's session prematurely!
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to run Supabase middleware', error)
|
||||
}
|
||||
|
||||
return supabaseResponse
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { createServerClient as createClient, type CookieOptions } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export function createServerClient() {
|
||||
const cookieStore = cookies()
|
||||
|
||||
try {
|
||||
return createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return cookieStore.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
try {
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
cookieStore.set(name, value, options)
|
||||
)
|
||||
} catch {
|
||||
// The `setAll` method was called from a Server Component.
|
||||
// This can be ignored if you have middleware refreshing
|
||||
// user sessions.
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
14
rnd/autogpt_builder/src/lib/types.ts
Normal file
14
rnd/autogpt_builder/src/lib/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
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[];
|
||||
};
|
||||
@@ -119,40 +119,3 @@ export function exportAsJSONFile(obj: object, filename: string): void {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { updateSession } from '@/lib/supabase/middleware'
|
||||
import { type NextRequest } from 'next/server'
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
return await updateSession(request)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except for the ones starting with:
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
* Feel free to modify this pattern to include more paths.
|
||||
*/
|
||||
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
||||
],
|
||||
}
|
||||
@@ -15,65 +15,21 @@ 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-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
'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;
|
||||
|
||||
@@ -224,11 +224,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||
|
||||
"@radix-ui/number@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.0.tgz#1e95610461a09cdf8bb05c152e76ca1278d5da46"
|
||||
integrity sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==
|
||||
|
||||
"@radix-ui/primitive@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2"
|
||||
@@ -251,20 +246,6 @@
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||
|
||||
"@radix-ui/react-collapsible@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz#4d49ddcc7b7d38f6c82f1fd29674f6fab5353e77"
|
||||
integrity sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.0"
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
"@radix-ui/react-context" "1.1.0"
|
||||
"@radix-ui/react-id" "1.1.0"
|
||||
"@radix-ui/react-presence" "1.1.0"
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||
|
||||
"@radix-ui/react-collection@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed"
|
||||
@@ -466,28 +447,6 @@
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
|
||||
"@radix-ui/react-scroll-area@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.1.0.tgz#50b24b0fc9ada151d176395bcf47b2ec68feada5"
|
||||
integrity sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==
|
||||
dependencies:
|
||||
"@radix-ui/number" "1.1.0"
|
||||
"@radix-ui/primitive" "1.1.0"
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
"@radix-ui/react-context" "1.1.0"
|
||||
"@radix-ui/react-direction" "1.1.0"
|
||||
"@radix-ui/react-presence" "1.1.0"
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||
|
||||
"@radix-ui/react-separator@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.0.tgz#ee0f4d86003b0e3ea7bc6ccab01ea0adee32663e"
|
||||
integrity sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==
|
||||
dependencies:
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
|
||||
"@radix-ui/react-slot@1.1.0", "@radix-ui/react-slot@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84"
|
||||
@@ -647,82 +606,11 @@
|
||||
classcat "^5.0.3"
|
||||
zustand "^4.4.1"
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@^4.9.5":
|
||||
version "4.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.1.tgz#0af2b6541ab0f4954d2c4f96bcdc7947420dd28c"
|
||||
integrity sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==
|
||||
|
||||
"@rushstack/eslint-patch@^1.3.3":
|
||||
version "1.10.3"
|
||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz#391d528054f758f81e53210f1a1eebcf1a8b1d20"
|
||||
integrity sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==
|
||||
|
||||
"@supabase/auth-js@2.64.4":
|
||||
version "2.64.4"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.64.4.tgz#f27fdabf1ebd1b532ceb57e8bbe66969ee09cfba"
|
||||
integrity sha512-9ITagy4WP4FLl+mke1rchapOH0RQpf++DI+WSG2sO1OFOZ0rW3cwAM0nCrMOxu+Zw4vJ4zObc08uvQrXx590Tg==
|
||||
dependencies:
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
|
||||
"@supabase/functions-js@2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.4.1.tgz#373e75f8d3453bacd71fb64f88d7a341d7b53ad7"
|
||||
integrity sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA==
|
||||
dependencies:
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
|
||||
"@supabase/node-fetch@2.6.15", "@supabase/node-fetch@^2.6.14":
|
||||
version "2.6.15"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/node-fetch/-/node-fetch-2.6.15.tgz#731271430e276983191930816303c44159e7226c"
|
||||
integrity sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
"@supabase/postgrest-js@1.15.8":
|
||||
version "1.15.8"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/postgrest-js/-/postgrest-js-1.15.8.tgz#827aaa408cdbc89e67d0a758e7a545ac86e34312"
|
||||
integrity sha512-YunjXpoQjQ0a0/7vGAvGZA2dlMABXFdVI/8TuVKtlePxyT71sl6ERl6ay1fmIeZcqxiuFQuZw/LXUuStUG9bbg==
|
||||
dependencies:
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
|
||||
"@supabase/realtime-js@2.10.2":
|
||||
version "2.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/realtime-js/-/realtime-js-2.10.2.tgz#c2b42d17d723d2d2a9146cfad61dc3df1ce3127e"
|
||||
integrity sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA==
|
||||
dependencies:
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
"@types/phoenix" "^1.5.4"
|
||||
"@types/ws" "^8.5.10"
|
||||
ws "^8.14.2"
|
||||
|
||||
"@supabase/ssr@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/ssr/-/ssr-0.4.0.tgz#3ecb607e5346e6e09d50c106c2335db0e97903dd"
|
||||
integrity sha512-6WS3NUvHDhCPAFN2kJ79AQDO8+M9fJ7y2fYpxgZqIuJEpnnGsHDNnB5Xnv8CiaJIuRU+0pKboy62RVZBMfZ0Lg==
|
||||
dependencies:
|
||||
cookie "^0.6.0"
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-linux-x64-gnu" "^4.9.5"
|
||||
|
||||
"@supabase/storage-js@2.6.0":
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/storage-js/-/storage-js-2.6.0.tgz#0fa5e04db760ed7f78e4394844a6d409e537adc5"
|
||||
integrity sha512-REAxr7myf+3utMkI2oOmZ6sdplMZZ71/2NEIEMBZHL9Fkmm3/JnaOZVSRqvG4LStYj2v5WhCruCzuMn6oD/Drw==
|
||||
dependencies:
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
|
||||
"@supabase/supabase-js@^2.45.0":
|
||||
version "2.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.45.0.tgz#d0778ab1a5a3ba2f4207e6c87cc6e288786ca0e5"
|
||||
integrity sha512-j66Mfs8RhzCQCKxKogAFQYH9oNhRmgIdKk6pexguI2Oc7hi+nL9UNJug5aL1tKnBdaBM3h65riPLQSdL6sWa3Q==
|
||||
dependencies:
|
||||
"@supabase/auth-js" "2.64.4"
|
||||
"@supabase/functions-js" "2.4.1"
|
||||
"@supabase/node-fetch" "2.6.15"
|
||||
"@supabase/postgrest-js" "1.15.8"
|
||||
"@supabase/realtime-js" "2.10.2"
|
||||
"@supabase/storage-js" "2.6.0"
|
||||
|
||||
"@swc/counter@^0.1.3":
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
|
||||
@@ -994,13 +882,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433"
|
||||
integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==
|
||||
|
||||
"@types/node@*":
|
||||
version "22.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.0.0.tgz#04862a2a71e62264426083abe1e27e87cac05a30"
|
||||
integrity sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==
|
||||
dependencies:
|
||||
undici-types "~6.11.1"
|
||||
|
||||
"@types/node@^20":
|
||||
version "20.14.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.9.tgz#12e8e765ab27f8c421a1820c99f5f313a933b420"
|
||||
@@ -1008,11 +889,6 @@
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/phoenix@^1.5.4":
|
||||
version "1.6.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/phoenix/-/phoenix-1.6.5.tgz#5654e14ec7ad25334a157a20015996b6d7d2075e"
|
||||
integrity sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
|
||||
@@ -1050,13 +926,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc"
|
||||
integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==
|
||||
|
||||
"@types/ws@^8.5.10":
|
||||
version "8.5.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e"
|
||||
integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.2.0.tgz#44356312aea8852a3a82deebdacd52ba614ec07a"
|
||||
@@ -1128,16 +997,6 @@ ajv@^6.12.4:
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ajv@^8.17.1:
|
||||
version "8.17.1"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
|
||||
integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.3"
|
||||
fast-uri "^3.0.1"
|
||||
json-schema-traverse "^1.0.0"
|
||||
require-from-string "^2.0.2"
|
||||
|
||||
ansi-regex@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||
@@ -1497,11 +1356,6 @@ concat-map@0.0.1:
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
cookie@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
|
||||
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
|
||||
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.2:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
@@ -2221,11 +2075,6 @@ fast-levenshtein@^2.0.6:
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
||||
|
||||
fast-uri@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134"
|
||||
integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.17.1"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"
|
||||
@@ -2854,11 +2703,6 @@ json-schema-traverse@^0.4.1:
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
|
||||
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
|
||||
|
||||
json-schema-traverse@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
|
||||
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
|
||||
|
||||
json-stable-stringify-without-jsonify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
@@ -3682,11 +3526,6 @@ react-hook-form@^7.52.1:
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.52.1.tgz#ec2c96437b977f8b89ae2d541a70736c66284852"
|
||||
integrity sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==
|
||||
|
||||
react-icons@^5.2.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.2.1.tgz#28c2040917b2a2eda639b0f797bff1888e018e4a"
|
||||
integrity sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==
|
||||
|
||||
react-is@^16.10.2, react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
@@ -3873,11 +3712,6 @@ remark-rehype@^11.0.0:
|
||||
unified "^11.0.0"
|
||||
vfile "^6.0.0"
|
||||
|
||||
require-from-string@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
||||
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
@@ -4272,11 +4106,6 @@ to-regex-range@^5.0.1:
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
trim-lines@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
|
||||
@@ -4388,11 +4217,6 @@ undici-types@~5.26.4:
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
|
||||
undici-types@~6.11.1:
|
||||
version "6.11.1"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.11.1.tgz#432ea6e8efd54a48569705a699e62d8f4981b197"
|
||||
integrity sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==
|
||||
|
||||
unified@^11.0.0:
|
||||
version "11.0.5"
|
||||
resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.5.tgz#f66677610a5c0a9ee90cab2b8d4d66037026d9e1"
|
||||
@@ -4528,19 +4352,6 @@ warning@^4.0.3:
|
||||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
|
||||
dependencies:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
which-boxed-primitive@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
|
||||
@@ -4626,11 +4437,6 @@ wrappy@1:
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
ws@^8.14.2:
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
|
||||
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
|
||||
|
||||
yaml@^2.3.4:
|
||||
version "2.4.5"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# AutoGPT Libs
|
||||
|
||||
This is a new project to store shared functionality across different services in NextGen AutoGPT (e.g. authentication)
|
||||
@@ -1,16 +0,0 @@
|
||||
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()
|
||||
@@ -1,20 +0,0 @@
|
||||
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)}")
|
||||
@@ -1,26 +0,0 @@
|
||||
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
37
rnd/autogpt_libs/poetry.lock
generated
@@ -1,37 +0,0 @@
|
||||
# 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"
|
||||
@@ -1,17 +0,0 @@
|
||||
[tool.poetry]
|
||||
name = "autogpt-libs"
|
||||
version = "0.1.0"
|
||||
description = "Shared libraries across NextGen AutoGPT"
|
||||
authors = ["Aarushi <aarushik93@gmail.com>"]
|
||||
readme = "README.md"
|
||||
packages = [{ include = "autogpt_libs" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10,<4.0"
|
||||
pyjwt = "^2.8.0"
|
||||
python-dotenv = "^1.0.1"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
@@ -4,4 +4,3 @@ DB_NAME=agpt_local
|
||||
DB_PORT=5432
|
||||
DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/${DB_NAME}"
|
||||
PRISMA_SCHEMA="postgres/schema.prisma"
|
||||
ENABLE_AUTH="false"
|
||||
|
||||
@@ -8,6 +8,3 @@ REDDIT_CLIENT_ID=
|
||||
REDDIT_CLIENT_SECRET=
|
||||
REDDIT_USERNAME=
|
||||
REDDIT_PASSWORD=
|
||||
|
||||
# Discord
|
||||
DISCORD_BOT_TOKEN=
|
||||
@@ -31,9 +31,9 @@ We use the Poetry to manage the dependencies. To set up the project, follow thes
|
||||
4. Copy .env.example to .env
|
||||
|
||||
```sh
|
||||
cp .env.example .env
|
||||
cp env.example .env
|
||||
```
|
||||
|
||||
|
||||
5. Generate the Prisma client
|
||||
|
||||
```sh
|
||||
@@ -72,4 +72,4 @@ Run the following command:
|
||||
|
||||
```sh
|
||||
poetry run app
|
||||
```
|
||||
```
|
||||
@@ -3,10 +3,6 @@
|
||||
This is an initial project for creating the next generation of agent execution, which is an AutoGPT agent server.
|
||||
The agent server will enable the creation of composite multi-agent systems that utilize AutoGPT agents and other non-agent components as its primitives.
|
||||
|
||||
## Docs
|
||||
|
||||
You can access the docs for the [AutoGPT Agent Server here](https://docs.agpt.co/server/setup).
|
||||
|
||||
## Setup
|
||||
|
||||
We use the Poetry to manage the dependencies. To set up the project, follow these steps inside this directory:
|
||||
@@ -53,7 +49,7 @@ We use the Poetry to manage the dependencies. To set up the project, follow thes
|
||||
|
||||
```sh
|
||||
poetry run prisma migrate dev
|
||||
```
|
||||
```
|
||||
|
||||
## Running The Server
|
||||
|
||||
|
||||
@@ -28,13 +28,14 @@ def run_processes(processes: list[AppProcess], **kwargs):
|
||||
|
||||
|
||||
def main(**kwargs):
|
||||
settings = get_config_and_secrets()
|
||||
set_start_method("spawn", force=True)
|
||||
freeze_support()
|
||||
|
||||
run_processes(
|
||||
[
|
||||
PyroNameServer(),
|
||||
ExecutionManager(),
|
||||
ExecutionManager(pool_size=settings.config.num_workers),
|
||||
ExecutionScheduler(),
|
||||
AgentServer(),
|
||||
],
|
||||
|
||||
@@ -20,19 +20,7 @@ for module in modules:
|
||||
|
||||
# Load all Block instances from the available modules
|
||||
AVAILABLE_BLOCKS = {}
|
||||
|
||||
|
||||
def all_subclasses(clz):
|
||||
subclasses = clz.__subclasses__()
|
||||
for subclass in subclasses:
|
||||
subclasses += all_subclasses(subclass)
|
||||
return subclasses
|
||||
|
||||
|
||||
for cls in all_subclasses(Block):
|
||||
if not cls.__name__.endswith("Block"):
|
||||
continue
|
||||
|
||||
for cls in Block.__subclasses__():
|
||||
block = cls()
|
||||
|
||||
if not isinstance(block.id, str) or len(block.id) != 36:
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Generic, List, TypeVar
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from autogpt_server.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||
from autogpt_server.data.model import SchemaField
|
||||
from autogpt_server.util.mock import MockObject
|
||||
|
||||
|
||||
class ValueBlock(Block):
|
||||
@@ -89,39 +86,28 @@ class PrintingBlock(Block):
|
||||
yield "status", "printed"
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
class ObjectLookupBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
input: Any = Field(description="Dictionary to lookup from")
|
||||
key: str | int = Field(description="Key to lookup in the dictionary")
|
||||
|
||||
class Output(BlockSchema):
|
||||
output: Any = Field(description="Value found for the given key")
|
||||
missing: Any = Field(description="Value of the input that missing the key")
|
||||
|
||||
class ObjectLookupBaseInput(BlockSchema, Generic[T]):
|
||||
input: T = Field(description="Dictionary to lookup from")
|
||||
key: str | int = Field(description="Key to lookup in the dictionary")
|
||||
|
||||
|
||||
class ObjectLookupBaseOutput(BlockSchema, Generic[T]):
|
||||
output: T = Field(description="Value found for the given key")
|
||||
missing: T = Field(description="Value of the input that missing the key")
|
||||
|
||||
|
||||
class ObjectLookupBase(Block, ABC, Generic[T]):
|
||||
@abstractmethod
|
||||
def block_id(self) -> str:
|
||||
pass
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
input_schema = ObjectLookupBaseInput[T]
|
||||
output_schema = ObjectLookupBaseOutput[T]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id=self.block_id(),
|
||||
id="b2g2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6",
|
||||
description="Lookup the given key in the input dictionary/object/list and return the value.",
|
||||
input_schema=input_schema,
|
||||
output_schema=output_schema,
|
||||
categories={BlockCategory.BASIC},
|
||||
input_schema=ObjectLookupBlock.Input,
|
||||
output_schema=ObjectLookupBlock.Output,
|
||||
test_input=[
|
||||
{"input": {"apple": 1, "banana": 2, "cherry": 3}, "key": "banana"},
|
||||
{"input": {"x": 10, "y": 20, "z": 30}, "key": "w"},
|
||||
{"input": [1, 2, 3], "key": 1},
|
||||
{"input": [1, 2, 3], "key": 3},
|
||||
{"input": MockObject(value="!!", key="key"), "key": "key"},
|
||||
{"input": ObjectLookupBlock.Input(input="!!", key="key"), "key": "key"},
|
||||
{"input": [{"k1": "v1"}, {"k2": "v2"}, {"k1": "v3"}], "key": "k1"},
|
||||
],
|
||||
test_output=[
|
||||
@@ -132,11 +118,9 @@ class ObjectLookupBase(Block, ABC, Generic[T]):
|
||||
("output", "key"),
|
||||
("output", ["v1", "v3"]),
|
||||
],
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def run(self, input_data: ObjectLookupBaseInput[T]) -> BlockOutput:
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
obj = input_data.input
|
||||
key = input_data.key
|
||||
|
||||
@@ -155,165 +139,3 @@ class ObjectLookupBase(Block, ABC, Generic[T]):
|
||||
yield "output", getattr(obj, key)
|
||||
else:
|
||||
yield "missing", input_data.input
|
||||
|
||||
|
||||
class ObjectLookupBlock(ObjectLookupBase[Any]):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(categories={BlockCategory.BASIC})
|
||||
|
||||
def block_id(self) -> str:
|
||||
return "b2g2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6"
|
||||
|
||||
|
||||
class InputBlock(ObjectLookupBase[Any]):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(categories={BlockCategory.BASIC, BlockCategory.INPUT_OUTPUT})
|
||||
|
||||
def block_id(self) -> str:
|
||||
return "c0a8e994-ebf1-4a9c-a4d8-89d09c86741b"
|
||||
|
||||
|
||||
class OutputBlock(ObjectLookupBase[Any]):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(categories={BlockCategory.BASIC, BlockCategory.INPUT_OUTPUT})
|
||||
|
||||
def block_id(self) -> str:
|
||||
return "363ae599-353e-4804-937e-b2ee3cef3da4"
|
||||
|
||||
|
||||
class DictionaryAddEntryBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
dictionary: dict | None = SchemaField(
|
||||
default=None,
|
||||
description="The dictionary to add the entry to. If not provided, a new dictionary will be created.",
|
||||
placeholder='{"key1": "value1", "key2": "value2"}',
|
||||
)
|
||||
key: str = SchemaField(
|
||||
description="The key for the new entry.", placeholder="new_key"
|
||||
)
|
||||
value: Any = SchemaField(
|
||||
description="The value for the new entry.", placeholder="new_value"
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
updated_dictionary: dict = SchemaField(
|
||||
description="The dictionary with the new entry added."
|
||||
)
|
||||
error: str = SchemaField(description="Error message if the operation failed.")
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="31d1064e-7446-4693-a7d4-65e5ca1180d1",
|
||||
description="Adds a new key-value pair to a dictionary. If no dictionary is provided, a new one is created.",
|
||||
categories={BlockCategory.BASIC},
|
||||
input_schema=DictionaryAddEntryBlock.Input,
|
||||
output_schema=DictionaryAddEntryBlock.Output,
|
||||
test_input=[
|
||||
{
|
||||
"dictionary": {"existing_key": "existing_value"},
|
||||
"key": "new_key",
|
||||
"value": "new_value",
|
||||
},
|
||||
{"key": "first_key", "value": "first_value"},
|
||||
],
|
||||
test_output=[
|
||||
(
|
||||
"updated_dictionary",
|
||||
{"existing_key": "existing_value", "new_key": "new_value"},
|
||||
),
|
||||
("updated_dictionary", {"first_key": "first_value"}),
|
||||
],
|
||||
)
|
||||
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
try:
|
||||
# If no dictionary is provided, create a new one
|
||||
if input_data.dictionary is None:
|
||||
updated_dict = {}
|
||||
else:
|
||||
# Create a copy of the input dictionary to avoid modifying the original
|
||||
updated_dict = input_data.dictionary.copy()
|
||||
|
||||
# Add the new key-value pair
|
||||
updated_dict[input_data.key] = input_data.value
|
||||
|
||||
yield "updated_dictionary", updated_dict
|
||||
except Exception as e:
|
||||
yield "error", f"Failed to add entry to dictionary: {str(e)}"
|
||||
|
||||
|
||||
class ListAddEntryBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
list: List[Any] | None = SchemaField(
|
||||
default=None,
|
||||
description="The list to add the entry to. If not provided, a new list will be created.",
|
||||
placeholder='[1, "string", {"key": "value"}]',
|
||||
)
|
||||
entry: Any = SchemaField(
|
||||
description="The entry to add to the list. Can be of any type (string, int, dict, etc.).",
|
||||
placeholder='{"new_key": "new_value"}',
|
||||
)
|
||||
position: int | None = SchemaField(
|
||||
default=None,
|
||||
description="The position to insert the new entry. If not provided, the entry will be appended to the end of the list.",
|
||||
placeholder="0",
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
updated_list: List[Any] = SchemaField(
|
||||
description="The list with the new entry added."
|
||||
)
|
||||
error: str = SchemaField(description="Error message if the operation failed.")
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="aeb08fc1-2fc1-4141-bc8e-f758f183a822",
|
||||
description="Adds a new entry to a list. The entry can be of any type. If no list is provided, a new one is created.",
|
||||
categories={BlockCategory.BASIC},
|
||||
input_schema=ListAddEntryBlock.Input,
|
||||
output_schema=ListAddEntryBlock.Output,
|
||||
test_input=[
|
||||
{
|
||||
"list": [1, "string", {"existing_key": "existing_value"}],
|
||||
"entry": {"new_key": "new_value"},
|
||||
"position": 1,
|
||||
},
|
||||
{"entry": "first_entry"},
|
||||
{"list": ["a", "b", "c"], "entry": "d"},
|
||||
],
|
||||
test_output=[
|
||||
(
|
||||
"updated_list",
|
||||
[
|
||||
1,
|
||||
{"new_key": "new_value"},
|
||||
"string",
|
||||
{"existing_key": "existing_value"},
|
||||
],
|
||||
),
|
||||
("updated_list", ["first_entry"]),
|
||||
("updated_list", ["a", "b", "c", "d"]),
|
||||
],
|
||||
)
|
||||
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
try:
|
||||
# If no list is provided, create a new one
|
||||
if input_data.list is None:
|
||||
updated_list = []
|
||||
else:
|
||||
# Create a copy of the input list to avoid modifying the original
|
||||
updated_list = input_data.list.copy()
|
||||
|
||||
# Add the new entry
|
||||
if input_data.position is None:
|
||||
updated_list.append(input_data.entry)
|
||||
else:
|
||||
updated_list.insert(input_data.position, input_data.entry)
|
||||
|
||||
yield "updated_list", updated_list
|
||||
except Exception as e:
|
||||
yield "error", f"Failed to add entry to list: {str(e)}"
|
||||
|
||||
@@ -3,6 +3,7 @@ import re
|
||||
from typing import Type
|
||||
|
||||
from autogpt_server.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||
from autogpt_server.util.test import execute_block_test
|
||||
|
||||
|
||||
class BlockInstallationBlock(Block):
|
||||
@@ -56,9 +57,6 @@ class BlockInstallationBlock(Block):
|
||||
module = __import__(module_name, fromlist=[class_name])
|
||||
block_class: Type[Block] = getattr(module, class_name)
|
||||
block = block_class()
|
||||
|
||||
from autogpt_server.util.test import execute_block_test
|
||||
|
||||
execute_block_test(block)
|
||||
yield "success", "Block installed successfully."
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import re
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from autogpt_server.data.block import Block, BlockOutput, BlockSchema
|
||||
|
||||
class ChunkingConfig(BaseModel):
|
||||
chunk_size: int = Field(default=1000, description="Maximum number of characters per chunk")
|
||||
overlap: int = Field(default=100, description="Number of characters to overlap between chunks")
|
||||
split_on: Optional[str] = Field(default=None, description="Regular expression to split on (e.g., '\n\n' for paragraphs)")
|
||||
|
||||
class ChunkingBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
text: str = Field(description="Text to be chunked")
|
||||
config: ChunkingConfig = Field(description="Chunking configuration")
|
||||
|
||||
class Output(BlockSchema):
|
||||
chunks: List[str] = Field(description="List of text chunks")
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="7d9e8f3a-2b5c-4e1d-9f3a-2b5c4e1d9f3a",
|
||||
input_schema=ChunkingBlock.Input,
|
||||
output_schema=ChunkingBlock.Output,
|
||||
test_input={
|
||||
"text": "This is a long piece of text that needs to be chunked. " * 20,
|
||||
"config": {
|
||||
"chunk_size": 100,
|
||||
"overlap": 20,
|
||||
"split_on": None
|
||||
}
|
||||
},
|
||||
test_output=("chunks", [
|
||||
"This is a long piece of text that needs to be chunked. This is a long piece of text that needs to be chunked. ",
|
||||
"to be chunked. This is a long piece of text that needs to be chunked. This is a long piece of text that needs ",
|
||||
"text that needs to be chunked. This is a long piece of text that needs to be chunked. This is a long piece of ",
|
||||
"of text that needs to be chunked. This is a long piece of text that needs to be chunked. This is a long piece ",
|
||||
"piece of text that needs to be chunked. This is a long piece of text that needs to be chunked. This is a long "
|
||||
]),
|
||||
)
|
||||
|
||||
def chunk_text(self, text: str, config: ChunkingConfig) -> List[str]:
|
||||
if config.split_on:
|
||||
# Split on the specified pattern
|
||||
segments = re.split(config.split_on, text)
|
||||
chunks = []
|
||||
current_chunk = ""
|
||||
for segment in segments:
|
||||
if len(current_chunk) + len(segment) > config.chunk_size:
|
||||
if current_chunk:
|
||||
chunks.append(current_chunk.strip())
|
||||
current_chunk = segment
|
||||
else:
|
||||
current_chunk += (" " if current_chunk else "") + segment
|
||||
if current_chunk:
|
||||
chunks.append(current_chunk.strip())
|
||||
else:
|
||||
chunks = []
|
||||
start = 0
|
||||
while start < len(text):
|
||||
end = start + config.chunk_size
|
||||
chunk = text[start:end]
|
||||
chunks.append(chunk)
|
||||
start = end - config.overlap
|
||||
|
||||
return chunks
|
||||
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
chunks = self.chunk_text(input_data.text, input_data.config)
|
||||
yield "chunks", chunks
|
||||
@@ -1,205 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
from pydantic import Field
|
||||
|
||||
from autogpt_server.data.block import Block, BlockOutput, BlockSchema
|
||||
from autogpt_server.data.model import BlockSecret, SecretField
|
||||
|
||||
|
||||
class DiscordReaderBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
discord_bot_token: BlockSecret = SecretField(
|
||||
key="discord_bot_token", description="Discord bot token"
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
message_content: str = Field(description="The content of the message received")
|
||||
channel_name: str = Field(
|
||||
description="The name of the channel the message was received from"
|
||||
)
|
||||
username: str = Field(
|
||||
description="The username of the user who sent the message"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="d3f4g5h6-1i2j-3k4l-5m6n-7o8p9q0r1s2t", # Unique ID for the node
|
||||
input_schema=DiscordReaderBlock.Input, # Assign input schema
|
||||
output_schema=DiscordReaderBlock.Output, # Assign output schema
|
||||
test_input={"discord_bot_token": "test_token"},
|
||||
test_output=[
|
||||
(
|
||||
"message_content",
|
||||
"Hello!\n\nFile from user: example.txt\nContent: This is the content of the file.",
|
||||
),
|
||||
("channel_name", "general"),
|
||||
("username", "test_user"),
|
||||
],
|
||||
test_mock={
|
||||
"run_bot": lambda token: asyncio.Future() # Create a Future object for mocking
|
||||
},
|
||||
)
|
||||
|
||||
async def run_bot(self, token: str):
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
|
||||
client = discord.Client(intents=intents)
|
||||
|
||||
self.output_data = None
|
||||
self.channel_name = None
|
||||
self.username = None
|
||||
|
||||
@client.event
|
||||
async def on_ready():
|
||||
print(f"Logged in as {client.user}")
|
||||
|
||||
@client.event
|
||||
async def on_message(message):
|
||||
if message.author == client.user:
|
||||
return
|
||||
|
||||
self.output_data = message.content
|
||||
self.channel_name = message.channel.name
|
||||
self.username = message.author.name
|
||||
|
||||
if message.attachments:
|
||||
attachment = message.attachments[0] # Process the first attachment
|
||||
if attachment.filename.endswith((".txt", ".py")):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(attachment.url) as response:
|
||||
file_content = await response.text()
|
||||
self.output_data += f"\n\nFile from user: {attachment.filename}\nContent: {file_content}"
|
||||
|
||||
await client.close()
|
||||
|
||||
await client.start(token)
|
||||
|
||||
def run(self, input_data: "DiscordReaderBlock.Input") -> BlockOutput:
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
future = self.run_bot(input_data.discord_bot_token.get_secret_value())
|
||||
|
||||
# If it's a Future (mock), set the result
|
||||
if isinstance(future, asyncio.Future):
|
||||
future.set_result(
|
||||
{
|
||||
"output_data": "Hello!\n\nFile from user: example.txt\nContent: This is the content of the file.",
|
||||
"channel_name": "general",
|
||||
"username": "test_user",
|
||||
}
|
||||
)
|
||||
|
||||
result = loop.run_until_complete(future)
|
||||
|
||||
# For testing purposes, use the mocked result
|
||||
if isinstance(result, dict):
|
||||
self.output_data = result.get("output_data")
|
||||
self.channel_name = result.get("channel_name")
|
||||
self.username = result.get("username")
|
||||
|
||||
if (
|
||||
self.output_data is None
|
||||
or self.channel_name is None
|
||||
or self.username is None
|
||||
):
|
||||
raise ValueError("No message, channel name, or username received.")
|
||||
|
||||
yield "message_content", self.output_data
|
||||
yield "channel_name", self.channel_name
|
||||
yield "username", self.username
|
||||
|
||||
except discord.errors.LoginFailure as login_err:
|
||||
raise ValueError(f"Login error occurred: {login_err}")
|
||||
except Exception as e:
|
||||
raise ValueError(f"An error occurred: {e}")
|
||||
|
||||
|
||||
class DiscordMessageSenderBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
discord_bot_token: BlockSecret = SecretField(
|
||||
key="discord_bot_token", description="Discord bot token"
|
||||
)
|
||||
message_content: str = Field(description="The content of the message received")
|
||||
channel_name: str = Field(
|
||||
description="The name of the channel the message was received from"
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
status: str = Field(
|
||||
description="The status of the operation (e.g., 'Message sent', 'Error')"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="h1i2j3k4-5l6m-7n8o-9p0q-r1s2t3u4v5w6", # Unique ID for the node
|
||||
input_schema=DiscordMessageSenderBlock.Input, # Assign input schema
|
||||
output_schema=DiscordMessageSenderBlock.Output, # Assign output schema
|
||||
test_input={
|
||||
"discord_bot_token": "YOUR_DISCORD_BOT_TOKEN",
|
||||
"channel_name": "general",
|
||||
"message_content": "Hello, Discord!",
|
||||
},
|
||||
test_output=[("status", "Message sent")],
|
||||
test_mock={
|
||||
"send_message": lambda token, channel_name, message_content: asyncio.Future()
|
||||
},
|
||||
)
|
||||
|
||||
async def send_message(self, token: str, channel_name: str, message_content: str):
|
||||
intents = discord.Intents.default()
|
||||
intents.guilds = True # Required for fetching guild/channel information
|
||||
client = discord.Client(intents=intents)
|
||||
|
||||
@client.event
|
||||
async def on_ready():
|
||||
print(f"Logged in as {client.user}")
|
||||
for guild in client.guilds:
|
||||
for channel in guild.text_channels:
|
||||
if channel.name == channel_name:
|
||||
# Split message into chunks if it exceeds 2000 characters
|
||||
for chunk in self.chunk_message(message_content):
|
||||
await channel.send(chunk)
|
||||
self.output_data = "Message sent"
|
||||
await client.close()
|
||||
return
|
||||
|
||||
self.output_data = "Channel not found"
|
||||
await client.close()
|
||||
|
||||
await client.start(token)
|
||||
|
||||
def chunk_message(self, message: str, limit: int = 2000) -> list:
|
||||
"""Splits a message into chunks not exceeding the Discord limit."""
|
||||
return [message[i : i + limit] for i in range(0, len(message), limit)]
|
||||
|
||||
def run(self, input_data: "DiscordMessageSenderBlock.Input") -> BlockOutput:
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
future = self.send_message(
|
||||
input_data.discord_bot_token.get_secret_value(),
|
||||
input_data.channel_name,
|
||||
input_data.message_content,
|
||||
)
|
||||
|
||||
# If it's a Future (mock), set the result
|
||||
if isinstance(future, asyncio.Future):
|
||||
future.set_result("Message sent")
|
||||
|
||||
result = loop.run_until_complete(future)
|
||||
|
||||
# For testing purposes, use the mocked result
|
||||
if isinstance(result, str):
|
||||
self.output_data = result
|
||||
|
||||
if self.output_data is None:
|
||||
raise ValueError("No status message received.")
|
||||
|
||||
yield "status", self.output_data
|
||||
|
||||
except discord.errors.LoginFailure as login_err:
|
||||
raise ValueError(f"Login error occurred: {login_err}")
|
||||
except Exception as e:
|
||||
raise ValueError(f"An error occurred: {e}")
|
||||
@@ -1,65 +0,0 @@
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
import openai
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from autogpt_server.data.block import Block, BlockOutput, BlockSchema
|
||||
from autogpt_server.util import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class EmbeddingModel(str, Enum):
|
||||
ada_002 = "text-embedding-ada-002"
|
||||
|
||||
class EmbeddingConfig(BaseModel):
|
||||
model: EmbeddingModel
|
||||
api_key: str
|
||||
|
||||
class EmbeddingBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
config: EmbeddingConfig
|
||||
texts: List[str] = Field(description="List of texts to create embeddings for")
|
||||
|
||||
class Output(BlockSchema):
|
||||
embeddings: List[List[float]]
|
||||
error: str
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="8f7e9a1c-3b7a-4b0f-9f1a-1c3b7a4b0f9f",
|
||||
input_schema=EmbeddingBlock.Input,
|
||||
output_schema=EmbeddingBlock.Output,
|
||||
test_input={
|
||||
"config": {
|
||||
"model": "text-embedding-ada-002",
|
||||
"api_key": "fake-api-key",
|
||||
},
|
||||
"texts": ["Hello, world!", "AutoGPT is amazing"],
|
||||
},
|
||||
test_output=("embeddings", [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]),
|
||||
test_mock={"create_embeddings": lambda *args, **kwargs: [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_embeddings(api_key: str, model: EmbeddingModel, texts: List[str]) -> List[List[float]]:
|
||||
openai.api_key = api_key
|
||||
response = openai.embeddings.create(
|
||||
model=model,
|
||||
input=texts,
|
||||
)
|
||||
return [embedding.embedding for embedding in response.data]
|
||||
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
try:
|
||||
embeddings = self.create_embeddings(
|
||||
input_data.config.api_key,
|
||||
input_data.config.model,
|
||||
input_data.texts
|
||||
)
|
||||
yield "embeddings", embeddings
|
||||
except Exception as e:
|
||||
error_message = f"Error creating embeddings: {str(e)}"
|
||||
logger.error(error_message)
|
||||
yield "error", error_message
|
||||
@@ -1,101 +0,0 @@
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from autogpt_server.data.block import Block, BlockOutput, BlockSchema
|
||||
from autogpt_server.data.model import SchemaField
|
||||
|
||||
|
||||
class ComparisonOperator(Enum):
|
||||
EQUAL = "=="
|
||||
NOT_EQUAL = "!="
|
||||
GREATER_THAN = ">"
|
||||
LESS_THAN = "<"
|
||||
GREATER_THAN_OR_EQUAL = ">="
|
||||
LESS_THAN_OR_EQUAL = "<="
|
||||
|
||||
|
||||
class ConditionBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
value1: Any = SchemaField(
|
||||
description="Enter the first value for comparison",
|
||||
placeholder="For example: 10 or 'hello' or True",
|
||||
)
|
||||
operator: ComparisonOperator = SchemaField(
|
||||
description="Choose the comparison operator",
|
||||
placeholder="Select an operator",
|
||||
)
|
||||
value2: Any = SchemaField(
|
||||
description="Enter the second value for comparison",
|
||||
placeholder="For example: 20 or 'world' or False",
|
||||
)
|
||||
yes_value: Any = SchemaField(
|
||||
description="(Optional) Value to output if the condition is true. If not provided, value1 will be used.",
|
||||
placeholder="Leave empty to use value1, or enter a specific value",
|
||||
default=None,
|
||||
)
|
||||
no_value: Any = SchemaField(
|
||||
description="(Optional) Value to output if the condition is false. If not provided, value1 will be used.",
|
||||
placeholder="Leave empty to use value1, or enter a specific value",
|
||||
default=None,
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
result: bool = SchemaField(
|
||||
description="The result of the condition evaluation (True or False)"
|
||||
)
|
||||
yes_output: Any = SchemaField(
|
||||
description="The output value if the condition is true"
|
||||
)
|
||||
no_output: Any = SchemaField(
|
||||
description="The output value if the condition is false"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="715696a0-e1da-45c8-b209-c2fa9c3b0be6",
|
||||
input_schema=ConditionBlock.Input,
|
||||
output_schema=ConditionBlock.Output,
|
||||
description="Handles conditional logic based on comparison operators",
|
||||
test_input={
|
||||
"value1": 10,
|
||||
"operator": ComparisonOperator.GREATER_THAN.value,
|
||||
"value2": 5,
|
||||
"yes_value": "Greater",
|
||||
"no_value": "Not greater",
|
||||
},
|
||||
test_output=[
|
||||
("result", True),
|
||||
("yes_output", "Greater"),
|
||||
],
|
||||
)
|
||||
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
value1 = input_data.value1
|
||||
operator = input_data.operator
|
||||
value2 = input_data.value2
|
||||
yes_value = input_data.yes_value if input_data.yes_value is not None else value1
|
||||
no_value = input_data.no_value if input_data.no_value is not None else value1
|
||||
|
||||
comparison_funcs = {
|
||||
ComparisonOperator.EQUAL: lambda a, b: a == b,
|
||||
ComparisonOperator.NOT_EQUAL: lambda a, b: a != b,
|
||||
ComparisonOperator.GREATER_THAN: lambda a, b: a > b,
|
||||
ComparisonOperator.LESS_THAN: lambda a, b: a < b,
|
||||
ComparisonOperator.GREATER_THAN_OR_EQUAL: lambda a, b: a >= b,
|
||||
ComparisonOperator.LESS_THAN_OR_EQUAL: lambda a, b: a <= b,
|
||||
}
|
||||
|
||||
try:
|
||||
result = comparison_funcs[operator](value1, value2)
|
||||
|
||||
yield "result", result
|
||||
|
||||
if result:
|
||||
yield "yes_output", yes_value
|
||||
else:
|
||||
yield "no_output", no_value
|
||||
|
||||
except Exception:
|
||||
yield "result", None
|
||||
yield "yes_output", None
|
||||
yield "no_output", None
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import List, NamedTuple
|
||||
from typing import NamedTuple
|
||||
|
||||
import anthropic
|
||||
import ollama
|
||||
@@ -8,7 +8,7 @@ import openai
|
||||
from groq import Groq
|
||||
|
||||
from autogpt_server.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||
from autogpt_server.data.model import BlockSecret, SchemaField, SecretField
|
||||
from autogpt_server.data.model import BlockSecret, SecretField
|
||||
from autogpt_server.util import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -409,127 +409,3 @@ class TextSummarizerBlock(Block):
|
||||
).send(None)[
|
||||
1
|
||||
] # Get the first yielded value
|
||||
|
||||
|
||||
class MessageRole(str, Enum):
|
||||
SYSTEM = "system"
|
||||
USER = "user"
|
||||
ASSISTANT = "assistant"
|
||||
|
||||
|
||||
class Message(BlockSchema):
|
||||
role: MessageRole
|
||||
content: str
|
||||
|
||||
|
||||
class AdvancedLlmCallBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
messages: List[Message] = SchemaField(
|
||||
description="List of messages in the conversation.", min_items=1
|
||||
)
|
||||
model: LlmModel = SchemaField(
|
||||
default=LlmModel.GPT4_TURBO,
|
||||
description="The language model to use for the conversation.",
|
||||
)
|
||||
api_key: BlockSecret = SecretField(
|
||||
value="", description="API key for the chosen language model provider."
|
||||
)
|
||||
max_tokens: int | None = SchemaField(
|
||||
default=None,
|
||||
description="The maximum number of tokens to generate in the chat completion.",
|
||||
ge=1,
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
response: str = SchemaField(
|
||||
description="The model's response to the conversation."
|
||||
)
|
||||
error: str = SchemaField(description="Error message if the API call failed.")
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="c3d4e5f6-g7h8-i9j0-k1l2-m3n4o5p6q7r8",
|
||||
description="Advanced LLM call that takes a list of messages and sends them to the language model.",
|
||||
categories={BlockCategory.LLM},
|
||||
input_schema=AdvancedLlmCallBlock.Input,
|
||||
output_schema=AdvancedLlmCallBlock.Output,
|
||||
test_input={
|
||||
"messages": [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Who won the world series in 2020?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "The Los Angeles Dodgers won the World Series in 2020.",
|
||||
},
|
||||
{"role": "user", "content": "Where was it played?"},
|
||||
],
|
||||
"model": LlmModel.GPT4_TURBO,
|
||||
"api_key": "test_api_key",
|
||||
},
|
||||
test_output=(
|
||||
"response",
|
||||
"The 2020 World Series was played at Globe Life Field in Arlington, Texas.",
|
||||
),
|
||||
test_mock={
|
||||
"llm_call": lambda *args, **kwargs: "The 2020 World Series was played at Globe Life Field in Arlington, Texas."
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def llm_call(
|
||||
api_key: str,
|
||||
model: LlmModel,
|
||||
messages: List[dict[str, str]],
|
||||
max_tokens: int | None = None,
|
||||
) -> str:
|
||||
provider = model.metadata.provider
|
||||
|
||||
if provider == "openai":
|
||||
openai.api_key = api_key
|
||||
response = openai.chat.completions.create(
|
||||
model=model.value,
|
||||
messages=messages, # type: ignore
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
return response.choices[0].message.content or ""
|
||||
elif provider == "anthropic":
|
||||
client = anthropic.Anthropic(api_key=api_key)
|
||||
response = client.messages.create(
|
||||
model=model.value, max_tokens=max_tokens or 4096, messages=messages # type: ignore
|
||||
)
|
||||
return response.content[0].text if response.content else ""
|
||||
elif provider == "groq":
|
||||
client = Groq(api_key=api_key)
|
||||
response = client.chat.completions.create(
|
||||
model=model.value,
|
||||
messages=messages, # type: ignore
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
return response.choices[0].message.content or ""
|
||||
elif provider == "ollama":
|
||||
response = ollama.chat(
|
||||
model=model.value, messages=messages, stream=False # type: ignore
|
||||
)
|
||||
return response["message"]["content"]
|
||||
else:
|
||||
raise ValueError(f"Unsupported LLM provider: {provider}")
|
||||
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
try:
|
||||
api_key = (
|
||||
input_data.api_key.get_secret_value()
|
||||
or LlmApiKeys[input_data.model.metadata.provider].get_secret_value()
|
||||
)
|
||||
|
||||
messages = [message.model_dump() for message in input_data.messages]
|
||||
|
||||
response = self.llm_call(
|
||||
api_key=api_key,
|
||||
model=input_data.model,
|
||||
messages=messages,
|
||||
max_tokens=input_data.max_tokens,
|
||||
)
|
||||
|
||||
yield "response", response
|
||||
except Exception as e:
|
||||
yield "error", f"Error calling LLM: {str(e)}"
|
||||
|
||||
@@ -154,33 +154,3 @@ class TextFormatterBlock(Block):
|
||||
texts=input_data.texts,
|
||||
**input_data.named_texts,
|
||||
)
|
||||
|
||||
|
||||
class TextCombinerBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
input1: str = Field(description="First text input", default="a")
|
||||
input2: str = Field(description="Second text input", default="b")
|
||||
|
||||
class Output(BlockSchema):
|
||||
output: str = Field(description="Combined text")
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="e30a4d42-7b7d-4e6a-b36e-1f9b8e3b7d85",
|
||||
description="This block combines multiple input texts into a single output text.",
|
||||
categories={BlockCategory.TEXT},
|
||||
input_schema=TextCombinerBlock.Input,
|
||||
output_schema=TextCombinerBlock.Output,
|
||||
test_input=[
|
||||
{"input1": "Hello world I like ", "input2": "cake and to go for walks"},
|
||||
{"input1": "This is a test. ", "input2": "Let's see how it works."},
|
||||
],
|
||||
test_output=[
|
||||
("output", "Hello world I like cake and to go for walks"),
|
||||
("output", "This is a test. Let's see how it works."),
|
||||
],
|
||||
)
|
||||
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
combined_text = (input_data.input1 or "") + (input_data.input2 or "")
|
||||
yield "output", combined_text
|
||||
|
||||
@@ -22,7 +22,6 @@ class BlockCategory(Enum):
|
||||
TEXT = "Block that processes text data."
|
||||
SEARCH = "Block that searches or extracts information from the internet."
|
||||
BASIC = "Block that performs basic operations."
|
||||
INPUT_OUTPUT = "Block that interacts with input/output of the graph."
|
||||
|
||||
def dict(self) -> dict[str, str]:
|
||||
return {"category": self.name, "description": self.value}
|
||||
|
||||
@@ -2,7 +2,7 @@ from collections import defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from multiprocessing import Manager
|
||||
from typing import Any, Generic, TypeVar
|
||||
from typing import Any
|
||||
|
||||
from prisma.models import (
|
||||
AgentGraphExecution,
|
||||
@@ -16,11 +16,6 @@ from autogpt_server.data.block import BlockData, BlockInput, CompletedBlockOutpu
|
||||
from autogpt_server.util import json
|
||||
|
||||
|
||||
class GraphExecution(BaseModel):
|
||||
graph_exec_id: str
|
||||
start_node_execs: list["NodeExecution"]
|
||||
|
||||
|
||||
class NodeExecution(BaseModel):
|
||||
graph_exec_id: str
|
||||
node_exec_id: str
|
||||
@@ -36,10 +31,7 @@ class ExecutionStatus(str, Enum):
|
||||
FAILED = "FAILED"
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class ExecutionQueue(Generic[T]):
|
||||
class ExecutionQueue:
|
||||
"""
|
||||
Queue for managing the execution of agents.
|
||||
This will be shared between different processes
|
||||
@@ -48,11 +40,11 @@ class ExecutionQueue(Generic[T]):
|
||||
def __init__(self):
|
||||
self.queue = Manager().Queue()
|
||||
|
||||
def add(self, execution: T) -> T:
|
||||
def add(self, execution: NodeExecution) -> NodeExecution:
|
||||
self.queue.put(execution)
|
||||
return execution
|
||||
|
||||
def get(self) -> T:
|
||||
def get(self) -> NodeExecution:
|
||||
return self.queue.get()
|
||||
|
||||
def empty(self) -> bool:
|
||||
@@ -75,14 +67,9 @@ class ExecutionResult(BaseModel):
|
||||
|
||||
@staticmethod
|
||||
def from_db(execution: AgentNodeExecution):
|
||||
if execution.executionData:
|
||||
# Execution that has been queued for execution will persist its data.
|
||||
input_data = json.loads(execution.executionData)
|
||||
else:
|
||||
# For incomplete execution, executionData will not be yet available.
|
||||
input_data: BlockInput = defaultdict()
|
||||
for data in execution.Input or []:
|
||||
input_data[data.name] = json.loads(data.data)
|
||||
input_data: BlockInput = defaultdict()
|
||||
for data in execution.Input or []:
|
||||
input_data[data.name] = json.loads(data.data)
|
||||
|
||||
output_data: CompletedBlockOutput = defaultdict(list)
|
||||
for data in execution.Output or []:
|
||||
@@ -159,68 +146,45 @@ async def upsert_execution_input(
|
||||
node_id: str,
|
||||
graph_exec_id: str,
|
||||
input_name: str,
|
||||
input_data: Any,
|
||||
node_exec_id: str | None = None,
|
||||
) -> tuple[str, BlockInput]:
|
||||
data: Any,
|
||||
) -> str:
|
||||
"""
|
||||
Insert AgentNodeExecutionInputOutput record for as one of AgentNodeExecution.Input.
|
||||
If there is no AgentNodeExecution that has no `input_name` as input, create new one.
|
||||
|
||||
Args:
|
||||
node_id: The id of the AgentNode.
|
||||
graph_exec_id: The id of the AgentGraphExecution.
|
||||
input_name: The name of the input data.
|
||||
input_data: The input data to be inserted.
|
||||
node_exec_id: [Optional] The id of the AgentNodeExecution that has no `input_name` as input. If not provided, it will find the eligible incomplete AgentNodeExecution or create a new one.
|
||||
|
||||
Returns:
|
||||
* The id of the created or existing AgentNodeExecution.
|
||||
* Dict of node input data, key is the input name, value is the input data.
|
||||
The id of the created or existing AgentNodeExecution.
|
||||
"""
|
||||
existing_execution = await AgentNodeExecution.prisma().find_first(
|
||||
where={ # type: ignore
|
||||
**({"id": node_exec_id} if node_exec_id else {}),
|
||||
"agentNodeId": node_id,
|
||||
"agentGraphExecutionId": graph_exec_id,
|
||||
"executionStatus": ExecutionStatus.INCOMPLETE,
|
||||
"Input": {"every": {"name": {"not": input_name}}},
|
||||
},
|
||||
order={"addedTime": "asc"},
|
||||
include={"Input": True},
|
||||
)
|
||||
json_input_data = json.dumps(input_data)
|
||||
json_data = json.dumps(data)
|
||||
|
||||
if existing_execution:
|
||||
await AgentNodeExecutionInputOutput.prisma().create(
|
||||
data={
|
||||
"name": input_name,
|
||||
"data": json_input_data,
|
||||
"data": json_data,
|
||||
"referencedByInputExecId": existing_execution.id,
|
||||
}
|
||||
)
|
||||
return existing_execution.id, {
|
||||
**{
|
||||
input_data.name: json.loads(input_data.data)
|
||||
for input_data in existing_execution.Input or []
|
||||
},
|
||||
input_name: input_data,
|
||||
}
|
||||
return existing_execution.id
|
||||
|
||||
elif not node_exec_id:
|
||||
else:
|
||||
result = await AgentNodeExecution.prisma().create(
|
||||
data={
|
||||
"agentNodeId": node_id,
|
||||
"agentGraphExecutionId": graph_exec_id,
|
||||
"executionStatus": ExecutionStatus.INCOMPLETE,
|
||||
"Input": {"create": {"name": input_name, "data": json_input_data}},
|
||||
"Input": {"create": {"name": input_name, "data": json_data}},
|
||||
}
|
||||
)
|
||||
return result.id, {input_name: input_data}
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
f"NodeExecution {node_exec_id} not found or already has input {input_name}."
|
||||
)
|
||||
return result.id
|
||||
|
||||
|
||||
async def upsert_execution_output(
|
||||
@@ -241,11 +205,8 @@ async def upsert_execution_output(
|
||||
|
||||
|
||||
async def update_execution_status(
|
||||
node_exec_id: str, status: ExecutionStatus, execution_data: BlockInput | None = None
|
||||
node_exec_id: str, status: ExecutionStatus
|
||||
) -> ExecutionResult:
|
||||
if status == ExecutionStatus.QUEUED and execution_data is None:
|
||||
raise ValueError("Execution data must be provided when queuing an execution.")
|
||||
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
data = {
|
||||
**({"executionStatus": status}),
|
||||
@@ -253,7 +214,6 @@ async def update_execution_status(
|
||||
**({"startedTime": now} if status == ExecutionStatus.RUNNING else {}),
|
||||
**({"endedTime": now} if status == ExecutionStatus.FAILED else {}),
|
||||
**({"endedTime": now} if status == ExecutionStatus.COMPLETED else {}),
|
||||
**({"executionData": json.dumps(execution_data)} if execution_data else {}),
|
||||
}
|
||||
|
||||
res = await AgentNodeExecution.prisma().update(
|
||||
@@ -279,15 +239,32 @@ async def get_execution_results(graph_exec_id: str) -> list[ExecutionResult]:
|
||||
executions = await AgentNodeExecution.prisma().find_many(
|
||||
where={"agentGraphExecutionId": graph_exec_id},
|
||||
include=EXECUTION_RESULT_INCLUDE, # type: ignore
|
||||
order=[
|
||||
{"queuedTime": "asc"},
|
||||
{"addedTime": "asc"}, # Fallback: Incomplete execs has no queuedTime.
|
||||
],
|
||||
order={"addedTime": "asc"},
|
||||
)
|
||||
res = [ExecutionResult.from_db(execution) for execution in executions]
|
||||
return res
|
||||
|
||||
|
||||
async def get_node_execution_input(node_exec_id: str) -> BlockInput:
|
||||
"""
|
||||
Get execution node input data from the previous node execution result.
|
||||
|
||||
Returns:
|
||||
dictionary of input data, key is the input name, value is the input data.
|
||||
"""
|
||||
execution = await AgentNodeExecution.prisma().find_unique_or_raise(
|
||||
where={"id": node_exec_id},
|
||||
include=EXECUTION_RESULT_INCLUDE, # type: ignore
|
||||
)
|
||||
if not execution.AgentNode:
|
||||
raise ValueError(f"Node {execution.agentNodeId} not found.")
|
||||
|
||||
return {
|
||||
input_data.name: json.loads(input_data.data)
|
||||
for input_data in execution.Input or []
|
||||
}
|
||||
|
||||
|
||||
LIST_SPLIT = "_$_"
|
||||
DICT_SPLIT = "_#_"
|
||||
OBJC_SPLIT = "_@_"
|
||||
@@ -364,33 +341,3 @@ def merge_execution_input(data: BlockInput) -> BlockInput:
|
||||
setattr(data[name], index, value)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
async def get_latest_execution(node_id: str, graph_eid: str) -> ExecutionResult | None:
|
||||
execution = await AgentNodeExecution.prisma().find_first(
|
||||
where={ # type: ignore
|
||||
"agentNodeId": node_id,
|
||||
"agentGraphExecutionId": graph_eid,
|
||||
"executionStatus": {"not": ExecutionStatus.INCOMPLETE},
|
||||
"executionData": {"not": None},
|
||||
},
|
||||
order={"queuedTime": "desc"},
|
||||
include=EXECUTION_RESULT_INCLUDE, # type: ignore
|
||||
)
|
||||
if not execution:
|
||||
return None
|
||||
return ExecutionResult.from_db(execution)
|
||||
|
||||
|
||||
async def get_incomplete_executions(
|
||||
node_id: str, graph_eid: str
|
||||
) -> list[ExecutionResult]:
|
||||
executions = await AgentNodeExecution.prisma().find_many(
|
||||
where={ # type: ignore
|
||||
"agentNodeId": node_id,
|
||||
"agentGraphExecutionId": graph_eid,
|
||||
"executionStatus": ExecutionStatus.INCOMPLETE,
|
||||
},
|
||||
include=EXECUTION_RESULT_INCLUDE, # type: ignore
|
||||
)
|
||||
return [ExecutionResult.from_db(execution) for execution in executions]
|
||||
|
||||
@@ -7,8 +7,7 @@ import prisma.types
|
||||
from prisma.models import AgentGraph, AgentNode, AgentNodeLink
|
||||
from pydantic import PrivateAttr
|
||||
|
||||
from autogpt_server.blocks.basic import InputBlock, OutputBlock
|
||||
from autogpt_server.data.block import BlockInput, get_block
|
||||
from autogpt_server.data.block import BlockInput
|
||||
from autogpt_server.data.db import BaseDbModel
|
||||
from autogpt_server.util import json
|
||||
|
||||
@@ -18,7 +17,6 @@ class Link(BaseDbModel):
|
||||
sink_id: str
|
||||
source_name: str
|
||||
sink_name: str
|
||||
is_static: bool = False
|
||||
|
||||
@staticmethod
|
||||
def from_db(link: AgentNodeLink):
|
||||
@@ -28,7 +26,6 @@ class Link(BaseDbModel):
|
||||
source_id=link.agentNodeSourceId,
|
||||
sink_name=link.sinkName,
|
||||
sink_id=link.agentNodeSinkId,
|
||||
is_static=link.isStatic,
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
@@ -93,68 +90,7 @@ class Graph(GraphMeta):
|
||||
@property
|
||||
def starting_nodes(self) -> list[Node]:
|
||||
outbound_nodes = {link.sink_id for link in self.links}
|
||||
input_nodes = {
|
||||
v.id for v in self.nodes if isinstance(get_block(v.block_id), InputBlock)
|
||||
}
|
||||
return [
|
||||
node
|
||||
for node in self.nodes
|
||||
if node.id not in outbound_nodes or node.id in input_nodes
|
||||
]
|
||||
|
||||
@property
|
||||
def ending_nodes(self) -> list[Node]:
|
||||
return [v for v in self.nodes if isinstance(get_block(v.block_id), OutputBlock)]
|
||||
|
||||
def validate_graph(self):
|
||||
|
||||
def sanitize(name):
|
||||
return name.split("_#_")[0].split("_@_")[0].split("_$_")[0]
|
||||
|
||||
# Check if all required fields are filled or connected, except for InputBlock.
|
||||
for node in self.nodes:
|
||||
block = get_block(node.block_id)
|
||||
if block is None:
|
||||
raise ValueError(f"Invalid block {node.block_id} for node #{node.id}")
|
||||
|
||||
provided_inputs = set(
|
||||
[sanitize(name) for name in node.input_default]
|
||||
+ [sanitize(link.sink_name) for link in node.input_links]
|
||||
)
|
||||
for name in block.input_schema.get_required_fields():
|
||||
if name not in provided_inputs and not isinstance(block, InputBlock):
|
||||
raise ValueError(
|
||||
f"Node {block.name} #{node.id} required input missing: `{name}`"
|
||||
)
|
||||
|
||||
# Check if all links are connected compatible pin data type.
|
||||
for link in self.links:
|
||||
source_id = link.source_id
|
||||
sink_id = link.sink_id
|
||||
suffix = f"Link {source_id}<->{sink_id}"
|
||||
|
||||
source_node = next((v for v in self.nodes if v.id == source_id), None)
|
||||
if not source_node:
|
||||
raise ValueError(f"{suffix}, {source_id} is invalid node.")
|
||||
sink_node = next((v for v in self.nodes if v.id == sink_id), None)
|
||||
if not sink_node:
|
||||
raise ValueError(f"{suffix}, {sink_id} is invalid node.")
|
||||
|
||||
source_block = get_block(source_node.block_id)
|
||||
if not source_block:
|
||||
raise ValueError(f"{suffix}, {source_node.block_id} is invalid block.")
|
||||
sink_block = get_block(sink_node.block_id)
|
||||
if not sink_block:
|
||||
raise ValueError(f"{suffix}, {sink_node.block_id} is invalid block.")
|
||||
|
||||
source_name = sanitize(link.source_name)
|
||||
if source_name not in source_block.output_schema.get_fields():
|
||||
raise ValueError(f"{suffix}, `{source_name}` is invalid output pin.")
|
||||
sink_name = sanitize(link.sink_name)
|
||||
if sink_name not in sink_block.input_schema.get_fields():
|
||||
raise ValueError(f"{suffix}, `{sink_name}` is invalid input pin.")
|
||||
|
||||
# TODO: Add type compatibility check here.
|
||||
return [node for node in self.nodes if node.id not in outbound_nodes]
|
||||
|
||||
@staticmethod
|
||||
def from_db(graph: AgentGraph):
|
||||
@@ -288,6 +224,7 @@ async def create_graph(graph: Graph) -> Graph:
|
||||
}
|
||||
)
|
||||
|
||||
# TODO: replace bulk creation using create_many
|
||||
await asyncio.gather(
|
||||
*[
|
||||
AgentNode.prisma().create(
|
||||
@@ -313,7 +250,6 @@ async def create_graph(graph: Graph) -> Graph:
|
||||
"sinkName": link.sink_name,
|
||||
"agentNodeSourceId": link.source_id,
|
||||
"agentNodeSinkId": link.sink_id,
|
||||
"isStatic": link.is_static,
|
||||
}
|
||||
)
|
||||
for link in graph.links
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from concurrent.futures import Future, ProcessPoolExecutor, TimeoutError
|
||||
from contextlib import contextmanager
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from typing import TYPE_CHECKING, Any, Coroutine, Generator, TypeVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from autogpt_server.server.server import AgentServer
|
||||
|
||||
from autogpt_server.blocks.basic import InputBlock
|
||||
from autogpt_server.data import db
|
||||
from autogpt_server.data.block import Block, BlockData, BlockInput, get_block
|
||||
from autogpt_server.data.execution import ExecutionQueue, ExecutionStatus
|
||||
from autogpt_server.data.execution import NodeExecution as Execution
|
||||
from autogpt_server.data.execution import (
|
||||
ExecutionQueue,
|
||||
ExecutionStatus,
|
||||
GraphExecution,
|
||||
NodeExecution,
|
||||
create_graph_execution,
|
||||
get_incomplete_executions,
|
||||
get_latest_execution,
|
||||
get_node_execution_input,
|
||||
merge_execution_input,
|
||||
parse_execution_output,
|
||||
update_execution_status,
|
||||
@@ -26,21 +21,20 @@ from autogpt_server.data.execution import (
|
||||
)
|
||||
from autogpt_server.data.graph import Graph, Link, Node, get_graph, get_node
|
||||
from autogpt_server.util.service import AppService, expose, get_service_client
|
||||
from autogpt_server.util.settings import Config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_log_prefix(graph_eid: str, node_eid: str, block_name: str = "-"):
|
||||
return f"[ExecutionManager][graph-eid-{graph_eid}|node-eid-{node_eid}|{block_name}]"
|
||||
return f"[ExecutionManager] [graph-{graph_eid}|node-{node_eid}|{block_name}]"
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
ExecutionStream = Generator[NodeExecution, None, None]
|
||||
ExecutionStream = Generator[Execution, None, None]
|
||||
|
||||
|
||||
def execute_node(
|
||||
loop: asyncio.AbstractEventLoop, api_client: "AgentServer", data: NodeExecution
|
||||
loop: asyncio.AbstractEventLoop, api_client: "AgentServer", data: Execution
|
||||
) -> ExecutionStream:
|
||||
"""
|
||||
Execute a node in the graph. This will trigger a block execution on a node,
|
||||
@@ -64,8 +58,9 @@ def execute_node(
|
||||
return loop.run_until_complete(f)
|
||||
|
||||
def update_execution(status: ExecutionStatus):
|
||||
exec_update = wait(update_execution_status(node_exec_id, status))
|
||||
api_client.send_execution_update(exec_update.model_dump())
|
||||
api_client.send_execution_update(
|
||||
wait(update_execution_status(node_exec_id, status)).model_dump()
|
||||
)
|
||||
|
||||
node = wait(get_node(node_id))
|
||||
if not node:
|
||||
@@ -94,7 +89,7 @@ def execute_node(
|
||||
wait(upsert_execution_output(node_exec_id, output_name, output_data))
|
||||
update_execution(ExecutionStatus.COMPLETED)
|
||||
|
||||
for execution in _enqueue_next_nodes(
|
||||
for execution in enqueue_next_nodes(
|
||||
api_client=api_client,
|
||||
loop=loop,
|
||||
node=node,
|
||||
@@ -112,132 +107,67 @@ def execute_node(
|
||||
raise e
|
||||
|
||||
|
||||
@contextmanager
|
||||
def synchronized(api_client: "AgentServer", key: Any):
|
||||
api_client.acquire_lock(key)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
api_client.release_lock(key)
|
||||
|
||||
|
||||
def _enqueue_next_nodes(
|
||||
def enqueue_next_nodes(
|
||||
api_client: "AgentServer",
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
node: Node,
|
||||
output: BlockData,
|
||||
graph_exec_id: str,
|
||||
prefix: str,
|
||||
) -> list[NodeExecution]:
|
||||
) -> list[Execution]:
|
||||
def wait(f: Coroutine[T, Any, T]) -> T:
|
||||
return loop.run_until_complete(f)
|
||||
|
||||
def add_enqueued_execution(
|
||||
node_exec_id: str, node_id: str, data: BlockInput
|
||||
) -> NodeExecution:
|
||||
exec_update = wait(
|
||||
update_execution_status(node_exec_id, ExecutionStatus.QUEUED, data)
|
||||
)
|
||||
api_client.send_execution_update(exec_update.model_dump())
|
||||
return NodeExecution(
|
||||
graph_exec_id=graph_exec_id,
|
||||
node_exec_id=node_exec_id,
|
||||
node_id=node_id,
|
||||
data=data,
|
||||
def execution_update(node_exec_id: str, status: ExecutionStatus):
|
||||
api_client.send_execution_update(
|
||||
wait(update_execution_status(node_exec_id, status)).model_dump()
|
||||
)
|
||||
|
||||
def register_next_executions(node_link: Link) -> list[NodeExecution]:
|
||||
enqueued_executions = []
|
||||
def update_execution_result(node_link: Link) -> Execution | None:
|
||||
next_output_name = node_link.source_name
|
||||
next_input_name = node_link.sink_name
|
||||
next_node_id = node_link.sink_id
|
||||
|
||||
next_data = parse_execution_output(output, next_output_name)
|
||||
if next_data is None:
|
||||
return enqueued_executions
|
||||
return
|
||||
|
||||
next_node = wait(get_node(next_node_id))
|
||||
if not next_node:
|
||||
logger.error(f"{prefix} Error, next node {next_node_id} not found.")
|
||||
return enqueued_executions
|
||||
return
|
||||
|
||||
# Multiple node can register the same next node, we need this to be atomic
|
||||
# To avoid same execution to be enqueued multiple times,
|
||||
# Or the same input to be consumed multiple times.
|
||||
with synchronized(api_client, ("upsert_input", next_node_id, graph_exec_id)):
|
||||
|
||||
# Add output data to the earliest incomplete execution, or create a new one.
|
||||
next_node_exec_id, next_node_input = wait(
|
||||
upsert_execution_input(
|
||||
node_id=next_node_id,
|
||||
graph_exec_id=graph_exec_id,
|
||||
input_name=next_input_name,
|
||||
input_data=next_data,
|
||||
)
|
||||
next_node_exec_id = wait(
|
||||
upsert_execution_input(
|
||||
node_id=next_node_id,
|
||||
graph_exec_id=graph_exec_id,
|
||||
input_name=next_input_name,
|
||||
data=next_data,
|
||||
)
|
||||
)
|
||||
|
||||
# Complete missing static input pins data using the last execution input.
|
||||
static_link_names = {
|
||||
link.sink_name
|
||||
for link in next_node.input_links
|
||||
if link.is_static and link.sink_name not in next_node_input
|
||||
}
|
||||
if static_link_names and (
|
||||
latest_execution := wait(
|
||||
get_latest_execution(next_node_id, graph_exec_id)
|
||||
)
|
||||
):
|
||||
for name in static_link_names:
|
||||
next_node_input[name] = latest_execution.input_data.get(name)
|
||||
next_node_input = wait(get_node_execution_input(next_node_exec_id))
|
||||
next_node_input, validation_msg = validate_exec(next_node, next_node_input)
|
||||
suffix = f"{next_output_name}~{next_input_name}#{next_node_id}:{validation_msg}"
|
||||
|
||||
# Validate the input data for the next node.
|
||||
next_node_input, validation_msg = validate_exec(next_node, next_node_input)
|
||||
suffix = f"{next_output_name}>{next_input_name}~{next_node_exec_id}:{validation_msg}"
|
||||
if not next_node_input:
|
||||
logger.warning(f"{prefix} Skipped queueing {suffix}")
|
||||
return
|
||||
|
||||
# Incomplete input data, skip queueing the execution.
|
||||
if not next_node_input:
|
||||
logger.warning(f"{prefix} Skipped queueing {suffix}")
|
||||
return enqueued_executions
|
||||
|
||||
# Input is complete, enqueue the execution.
|
||||
logger.warning(f"{prefix} Enqueued {suffix}")
|
||||
enqueued_executions.append(
|
||||
add_enqueued_execution(next_node_exec_id, next_node_id, next_node_input)
|
||||
)
|
||||
|
||||
# Next execution stops here if the link is not static.
|
||||
if not node_link.is_static:
|
||||
return enqueued_executions
|
||||
|
||||
# If link is static, there could be some incomplete executions waiting for it.
|
||||
# Load and complete the input missing input data, and try to re-enqueue them.
|
||||
for iexec in wait(get_incomplete_executions(next_node_id, graph_exec_id)):
|
||||
idata = iexec.input_data
|
||||
ineid = iexec.node_exec_id
|
||||
|
||||
static_link_names = {
|
||||
link.sink_name
|
||||
for link in next_node.input_links
|
||||
if link.is_static and link.sink_name not in idata
|
||||
}
|
||||
for input_name in static_link_names:
|
||||
idata[input_name] = next_node_input[input_name]
|
||||
|
||||
idata, msg = validate_exec(next_node, idata)
|
||||
suffix = f"{next_output_name}>{next_input_name}~{ineid}:{msg}"
|
||||
if not idata:
|
||||
logger.warning(f"{prefix} Re-enqueueing skipped: {suffix}")
|
||||
continue
|
||||
logger.warning(f"{prefix} Re-enqueued {suffix}")
|
||||
enqueued_executions.append(
|
||||
add_enqueued_execution(iexec.node_exec_id, next_node_id, idata)
|
||||
)
|
||||
return enqueued_executions
|
||||
# Input is complete, enqueue the execution.
|
||||
logger.warning(f"{prefix} Enqueued {suffix}")
|
||||
execution_update(next_node_exec_id, ExecutionStatus.QUEUED)
|
||||
return Execution(
|
||||
graph_exec_id=graph_exec_id,
|
||||
node_exec_id=next_node_exec_id,
|
||||
node_id=next_node.id,
|
||||
data=next_node_input,
|
||||
)
|
||||
|
||||
return [
|
||||
execution
|
||||
for link in node.output_links
|
||||
for execution in register_next_executions(link)
|
||||
if (execution := update_execution_result(link))
|
||||
]
|
||||
|
||||
|
||||
@@ -298,118 +228,44 @@ def get_agent_server_client() -> "AgentServer":
|
||||
|
||||
|
||||
class Executor:
|
||||
"""
|
||||
This class contains event handlers for the process pool executor events.
|
||||
|
||||
The main events are:
|
||||
on_node_executor_start: Initialize the process that executes the node.
|
||||
on_node_execution: Execution logic for a node.
|
||||
|
||||
on_graph_executor_start: Initialize the process that executes the graph.
|
||||
on_graph_execution: Execution logic for a graph.
|
||||
|
||||
The execution flow:
|
||||
1. Graph execution request is added to the queue.
|
||||
2. Graph executor loop picks the request from the queue.
|
||||
3. Graph executor loop submits the graph execution request to the executor pool.
|
||||
[on_graph_execution]
|
||||
4. Graph executor initialize the node execution queue.
|
||||
5. Graph executor adds the starting nodes to the node execution queue.
|
||||
6. Graph executor waits for all nodes to be executed.
|
||||
[on_node_execution]
|
||||
7. Node executor picks the node execution request from the queue.
|
||||
8. Node executor executes the node.
|
||||
9. Node executor enqueues the next executed nodes to the node execution queue.
|
||||
"""
|
||||
loop: asyncio.AbstractEventLoop
|
||||
|
||||
@classmethod
|
||||
def on_node_executor_start(cls):
|
||||
def on_executor_start(cls):
|
||||
cls.loop = asyncio.new_event_loop()
|
||||
cls.loop.run_until_complete(db.connect())
|
||||
cls.agent_server_client = get_agent_server_client()
|
||||
|
||||
@classmethod
|
||||
def on_node_execution(cls, q: ExecutionQueue[NodeExecution], data: NodeExecution):
|
||||
def on_start_execution(cls, q: ExecutionQueue, data: Execution) -> bool:
|
||||
prefix = get_log_prefix(data.graph_exec_id, data.node_exec_id)
|
||||
try:
|
||||
logger.warning(f"{prefix} Start node execution")
|
||||
logger.warning(f"{prefix} Start execution")
|
||||
for execution in execute_node(cls.loop, cls.agent_server_client, data):
|
||||
q.add(execution)
|
||||
logger.warning(f"{prefix} Finished node execution")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.exception(f"{prefix} Failed node execution: {e}")
|
||||
|
||||
@classmethod
|
||||
def on_graph_executor_start(cls):
|
||||
cls.pool_size = Config().num_node_workers
|
||||
cls.executor = ProcessPoolExecutor(
|
||||
max_workers=cls.pool_size,
|
||||
initializer=cls.on_node_executor_start,
|
||||
)
|
||||
logger.warning(f"Graph executor started with max-{cls.pool_size} node workers.")
|
||||
|
||||
@classmethod
|
||||
def on_graph_execution(cls, graph_data: GraphExecution):
|
||||
prefix = get_log_prefix(graph_data.graph_exec_id, "*")
|
||||
logger.warning(f"{prefix} Start graph execution")
|
||||
|
||||
try:
|
||||
queue = ExecutionQueue[NodeExecution]()
|
||||
for node_exec in graph_data.start_node_execs:
|
||||
queue.add(node_exec)
|
||||
|
||||
futures: dict[str, Future] = {}
|
||||
while not queue.empty():
|
||||
execution = queue.get()
|
||||
|
||||
# Avoid parallel execution of the same node.
|
||||
fut = futures.get(execution.node_id)
|
||||
if fut and not fut.done():
|
||||
cls.wait_future(fut)
|
||||
logger.warning(f"{prefix} Re-enqueueing {execution.node_id}")
|
||||
queue.add(execution)
|
||||
continue
|
||||
|
||||
futures[execution.node_id] = cls.executor.submit(
|
||||
cls.on_node_execution, queue, execution
|
||||
)
|
||||
|
||||
# Avoid terminating graph execution when some nodes are still running.
|
||||
while queue.empty() and futures:
|
||||
for node_id, future in list(futures.items()):
|
||||
if future.done():
|
||||
del futures[node_id]
|
||||
elif queue.empty():
|
||||
cls.wait_future(future)
|
||||
|
||||
logger.warning(f"{prefix} Finished graph execution")
|
||||
except Exception as e:
|
||||
logger.exception(f"{prefix} Failed graph execution: {e}")
|
||||
|
||||
@classmethod
|
||||
def wait_future(cls, future: Future):
|
||||
try:
|
||||
future.result(timeout=3)
|
||||
except TimeoutError:
|
||||
# Avoid being blocked by long-running node, by not waiting its completion.
|
||||
pass
|
||||
logger.exception(f"{prefix} Error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
class ExecutionManager(AppService):
|
||||
def __init__(self):
|
||||
self.pool_size = Config().num_graph_workers
|
||||
self.queue = ExecutionQueue[GraphExecution]()
|
||||
def __init__(self, pool_size: int):
|
||||
self.pool_size = pool_size
|
||||
self.queue = ExecutionQueue()
|
||||
|
||||
def run_service(self):
|
||||
with ProcessPoolExecutor(
|
||||
max_workers=self.pool_size,
|
||||
initializer=Executor.on_graph_executor_start,
|
||||
initializer=Executor.on_executor_start,
|
||||
) as executor:
|
||||
logger.warning(
|
||||
f"Execution manager started with max-{self.pool_size} graph workers."
|
||||
)
|
||||
logger.warning(f"Execution manager started with {self.pool_size} workers.")
|
||||
while True:
|
||||
executor.submit(Executor.on_graph_execution, self.queue.get())
|
||||
executor.submit(
|
||||
Executor.on_start_execution,
|
||||
self.queue,
|
||||
self.queue.get(),
|
||||
)
|
||||
|
||||
@property
|
||||
def agent_server_client(self) -> "AgentServer":
|
||||
@@ -420,16 +276,10 @@ class ExecutionManager(AppService):
|
||||
graph: Graph | None = self.run_and_wait(get_graph(graph_id))
|
||||
if not graph:
|
||||
raise Exception(f"Graph #{graph_id} not found.")
|
||||
graph.validate_graph()
|
||||
|
||||
nodes_input = []
|
||||
for node in graph.starting_nodes:
|
||||
if isinstance(get_block(node.block_id), InputBlock):
|
||||
input_data = {"input": data}
|
||||
else:
|
||||
input_data = {}
|
||||
|
||||
input_data, error = validate_exec(node, input_data)
|
||||
input_data, error = validate_exec(node, data)
|
||||
if not input_data:
|
||||
raise Exception(error)
|
||||
else:
|
||||
@@ -442,28 +292,32 @@ class ExecutionManager(AppService):
|
||||
nodes_input=nodes_input,
|
||||
)
|
||||
)
|
||||
|
||||
starting_node_execs = []
|
||||
executions: list[BlockInput] = []
|
||||
for node_exec in node_execs:
|
||||
starting_node_execs.append(
|
||||
NodeExecution(
|
||||
self.add_node_execution(
|
||||
Execution(
|
||||
graph_exec_id=node_exec.graph_exec_id,
|
||||
node_exec_id=node_exec.node_exec_id,
|
||||
node_id=node_exec.node_id,
|
||||
data=node_exec.input_data,
|
||||
)
|
||||
)
|
||||
exec_update = self.run_and_wait(
|
||||
update_execution_status(
|
||||
node_exec.node_exec_id, ExecutionStatus.QUEUED, node_exec.input_data
|
||||
)
|
||||
|
||||
executions.append(
|
||||
{
|
||||
"id": node_exec.node_exec_id,
|
||||
"node_id": node_exec.node_id,
|
||||
}
|
||||
)
|
||||
self.agent_server_client.send_execution_update(exec_update.model_dump())
|
||||
|
||||
graph_exec = GraphExecution(
|
||||
graph_exec_id=graph_exec_id,
|
||||
start_node_execs=starting_node_execs,
|
||||
return {
|
||||
"id": graph_exec_id,
|
||||
"executions": executions,
|
||||
}
|
||||
|
||||
def add_node_execution(self, execution: Execution) -> Execution:
|
||||
res = self.run_and_wait(
|
||||
update_execution_status(execution.node_exec_id, ExecutionStatus.QUEUED)
|
||||
)
|
||||
self.queue.add(graph_exec)
|
||||
|
||||
return {"id": graph_exec_id}
|
||||
self.agent_server_client.send_execution_update(res.model_dump())
|
||||
return self.queue.add(execution)
|
||||
|
||||
@@ -5,11 +5,9 @@ from contextlib import asynccontextmanager
|
||||
from typing import Annotated, Any, Dict
|
||||
|
||||
import uvicorn
|
||||
from autogpt_libs.auth.middleware import auth_middleware
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Body,
|
||||
Depends,
|
||||
FastAPI,
|
||||
HTTPException,
|
||||
WebSocket,
|
||||
@@ -17,16 +15,12 @@ from fastapi import (
|
||||
)
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
import autogpt_server.server.ws_api
|
||||
from autogpt_server.data import block, db
|
||||
from autogpt_server.data import block, db, execution
|
||||
from autogpt_server.data import graph as graph_db
|
||||
from autogpt_server.data.block import BlockInput, CompletedBlockOutput
|
||||
from autogpt_server.data.execution import (
|
||||
ExecutionResult,
|
||||
get_execution_results,
|
||||
list_executions,
|
||||
)
|
||||
from autogpt_server.executor import ExecutionManager, ExecutionScheduler
|
||||
from autogpt_server.server.conn_manager import ConnectionManager
|
||||
from autogpt_server.server.model import (
|
||||
@@ -35,27 +29,25 @@ from autogpt_server.server.model import (
|
||||
SetGraphActiveVersion,
|
||||
WsMessage,
|
||||
)
|
||||
from autogpt_server.util.lock import KeyedMutex
|
||||
from autogpt_server.util.data import get_frontend_path
|
||||
from autogpt_server.util.service import AppService, expose, get_service_client
|
||||
from autogpt_server.util.settings import Settings
|
||||
|
||||
|
||||
class AgentServer(AppService):
|
||||
event_queue: asyncio.Queue[ExecutionResult] = asyncio.Queue()
|
||||
event_queue: asyncio.Queue[execution.ExecutionResult] = asyncio.Queue()
|
||||
manager = ConnectionManager()
|
||||
mutex = KeyedMutex()
|
||||
use_db = False
|
||||
|
||||
async def event_broadcaster(self):
|
||||
while True:
|
||||
event: ExecutionResult = await self.event_queue.get()
|
||||
event: execution.ExecutionResult = await self.event_queue.get()
|
||||
await self.manager.send_execution_result(event)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(self, _: FastAPI):
|
||||
await db.connect()
|
||||
await block.initialize_blocks()
|
||||
await graph_db.import_packaged_templates()
|
||||
self.run_and_wait(block.initialize_blocks())
|
||||
self.run_and_wait(graph_db.import_packaged_templates())
|
||||
asyncio.create_task(self.event_broadcaster())
|
||||
yield
|
||||
await db.disconnect()
|
||||
@@ -82,8 +74,6 @@ class AgentServer(AppService):
|
||||
|
||||
# Define the API routes
|
||||
router = APIRouter(prefix="/api")
|
||||
router.dependencies.append(Depends(auth_middleware))
|
||||
|
||||
router.add_api_route(
|
||||
path="/blocks",
|
||||
endpoint=self.get_graph_blocks, # type: ignore
|
||||
@@ -193,6 +183,12 @@ class AgentServer(AppService):
|
||||
|
||||
app.add_exception_handler(500, self.handle_internal_error) # type: ignore
|
||||
|
||||
app.mount(
|
||||
path="/frontend",
|
||||
app=StaticFiles(directory=get_frontend_path(), html=True),
|
||||
name="example_files",
|
||||
)
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
@app.websocket("/ws")
|
||||
@@ -466,6 +462,9 @@ class AgentServer(AppService):
|
||||
status_code=400, detail="Either graph or template_id must be provided."
|
||||
)
|
||||
|
||||
# TODO: replace uuid generation here to DB generated uuids.
|
||||
graph.id = str(uuid.uuid4())
|
||||
|
||||
graph.is_template = is_template
|
||||
graph.is_active = not is_template
|
||||
|
||||
@@ -553,17 +552,17 @@ class AgentServer(AppService):
|
||||
status_code=404, detail=f"Agent #{graph_id}{rev} not found."
|
||||
)
|
||||
|
||||
return await list_executions(graph_id, graph_version)
|
||||
return await execution.list_executions(graph_id, graph_version)
|
||||
|
||||
@classmethod
|
||||
async def get_run_execution_results(
|
||||
cls, graph_id: str, run_id: str
|
||||
) -> list[ExecutionResult]:
|
||||
) -> list[execution.ExecutionResult]:
|
||||
graph = await graph_db.get_graph(graph_id)
|
||||
if not graph:
|
||||
raise HTTPException(status_code=404, detail=f"Graph #{graph_id} not found.")
|
||||
|
||||
return await get_execution_results(run_id)
|
||||
return await execution.get_execution_results(run_id)
|
||||
|
||||
async def create_schedule(
|
||||
self, graph_id: str, cron: str, input_data: dict[Any, Any]
|
||||
@@ -592,32 +591,14 @@ class AgentServer(AppService):
|
||||
|
||||
@expose
|
||||
def send_execution_update(self, execution_result_dict: dict[Any, Any]):
|
||||
execution_result = ExecutionResult(**execution_result_dict)
|
||||
execution_result = execution.ExecutionResult(**execution_result_dict)
|
||||
self.run_and_wait(self.event_queue.put(execution_result))
|
||||
|
||||
@expose
|
||||
def acquire_lock(self, key: Any):
|
||||
self.mutex.lock(key)
|
||||
|
||||
@expose
|
||||
def release_lock(self, key: Any):
|
||||
self.mutex.unlock(key)
|
||||
|
||||
@classmethod
|
||||
def update_configuration(
|
||||
cls,
|
||||
updated_settings: Annotated[
|
||||
Dict[str, Any],
|
||||
Body(
|
||||
examples=[
|
||||
{
|
||||
"config": {
|
||||
"num_graph_workers": 10,
|
||||
"num_node_workers": 10,
|
||||
}
|
||||
}
|
||||
]
|
||||
),
|
||||
Dict[str, Any], Body(examples=[{"config": {"num_workers": 10}}])
|
||||
],
|
||||
):
|
||||
settings = Settings()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from autogpt_server.blocks.basic import InputBlock, PrintingBlock
|
||||
from autogpt_server.blocks.basic import PrintingBlock, ValueBlock
|
||||
from autogpt_server.blocks.text import TextFormatterBlock
|
||||
from autogpt_server.data import graph
|
||||
from autogpt_server.data.graph import create_graph
|
||||
@@ -14,18 +14,12 @@ def create_test_graph() -> graph.Graph:
|
||||
ValueBlock
|
||||
"""
|
||||
nodes = [
|
||||
graph.Node(
|
||||
block_id=InputBlock().id,
|
||||
input_default={"key": "input_1"},
|
||||
),
|
||||
graph.Node(
|
||||
block_id=InputBlock().id,
|
||||
input_default={"key": "input_2"},
|
||||
),
|
||||
graph.Node(block_id=ValueBlock().id),
|
||||
graph.Node(block_id=ValueBlock().id),
|
||||
graph.Node(
|
||||
block_id=TextFormatterBlock().id,
|
||||
input_default={
|
||||
"format": "{texts[0]}, {texts[1]}{texts[2]}",
|
||||
"format": "{texts[0]},{texts[1]},{texts[2]}",
|
||||
"texts_$_3": "!!!",
|
||||
},
|
||||
),
|
||||
@@ -64,7 +58,7 @@ async def sample_agent():
|
||||
async with SpinTestServer() as server:
|
||||
exec_man = server.exec_manager
|
||||
test_graph = await create_graph(create_test_graph())
|
||||
input_data = {"input_1": "Hello", "input_2": "World"}
|
||||
input_data = {"input": "test!!"}
|
||||
response = await server.agent_server.execute_graph(test_graph.id, input_data)
|
||||
print(response)
|
||||
result = await wait_execution(exec_man, test_graph.id, response["id"], 4, 10)
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
from threading import Lock
|
||||
from typing import Any
|
||||
|
||||
from expiringdict import ExpiringDict
|
||||
|
||||
|
||||
class KeyedMutex:
|
||||
"""
|
||||
This class provides a mutex that can be locked and unlocked by a specific key.
|
||||
It uses an ExpiringDict to automatically clear the mutex after a specified timeout,
|
||||
in case the key is not unlocked for a specified duration, to prevent memory leaks.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.locks: dict[Any, Lock] = ExpiringDict(max_len=6000, max_age_seconds=60)
|
||||
self.locks_lock = Lock()
|
||||
|
||||
def lock(self, key: Any):
|
||||
with self.locks_lock:
|
||||
if key not in self.locks:
|
||||
self.locks[key] = (lock := Lock())
|
||||
else:
|
||||
lock = self.locks[key]
|
||||
lock.acquire()
|
||||
|
||||
def unlock(self, key: Any):
|
||||
with self.locks_lock:
|
||||
if key in self.locks:
|
||||
lock = self.locks.pop(key)
|
||||
else:
|
||||
return
|
||||
lock.release()
|
||||
@@ -11,14 +11,11 @@ from tenacity import retry, stop_after_delay, wait_exponential
|
||||
|
||||
from autogpt_server.data import db
|
||||
from autogpt_server.util.process import AppProcess
|
||||
from autogpt_server.util.settings import Config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
conn_retry = retry(stop=stop_after_delay(5), wait=wait_exponential(multiplier=0.1))
|
||||
T = TypeVar("T")
|
||||
|
||||
pyro_host = Config().pyro_host
|
||||
|
||||
|
||||
def expose(func: Callable) -> Callable:
|
||||
def wrapper(*args, **kwargs):
|
||||
@@ -36,14 +33,13 @@ class PyroNameServer(AppProcess):
|
||||
def run(self):
|
||||
try:
|
||||
print("Starting NameServer loop")
|
||||
nameserver.start_ns_loop(host=pyro_host, port=9090)
|
||||
nameserver.start_ns_loop()
|
||||
except KeyboardInterrupt:
|
||||
print("Shutting down NameServer")
|
||||
|
||||
|
||||
class AppService(AppProcess):
|
||||
shared_event_loop: asyncio.AbstractEventLoop
|
||||
use_db: bool = True
|
||||
|
||||
@classmethod
|
||||
@property
|
||||
@@ -55,17 +51,16 @@ class AppService(AppProcess):
|
||||
while True:
|
||||
time.sleep(10)
|
||||
|
||||
def __run_async(self, coro: Coroutine[T, Any, T]):
|
||||
def run_async(self, coro: Coroutine[T, Any, T]):
|
||||
return asyncio.run_coroutine_threadsafe(coro, self.shared_event_loop)
|
||||
|
||||
def run_and_wait(self, coro: Coroutine[T, Any, T]) -> T:
|
||||
future = self.__run_async(coro)
|
||||
future = self.run_async(coro)
|
||||
return future.result()
|
||||
|
||||
def run(self):
|
||||
self.shared_event_loop = asyncio.get_event_loop()
|
||||
if self.use_db:
|
||||
self.shared_event_loop.run_until_complete(db.connect())
|
||||
self.shared_event_loop.run_until_complete(db.connect())
|
||||
|
||||
# Initialize the async loop.
|
||||
async_thread = threading.Thread(target=self.__start_async_loop)
|
||||
@@ -82,14 +77,15 @@ class AppService(AppProcess):
|
||||
|
||||
@conn_retry
|
||||
def __start_pyro(self):
|
||||
daemon = pyro.Daemon(host=pyro_host)
|
||||
ns = pyro.locate_ns(host=pyro_host, port=9090)
|
||||
daemon = pyro.Daemon()
|
||||
ns = pyro.locate_ns()
|
||||
uri = daemon.register(self)
|
||||
ns.register(self.service_name, uri)
|
||||
logger.warning(f"Service [{self.service_name}] Ready. Object URI = {uri}")
|
||||
daemon.requestLoop()
|
||||
|
||||
def __start_async_loop(self):
|
||||
# asyncio.set_event_loop(self.shared_event_loop)
|
||||
self.shared_event_loop.run_forever()
|
||||
|
||||
|
||||
|
||||
@@ -41,21 +41,8 @@ class UpdateTrackingModel(BaseModel, Generic[T]):
|
||||
class Config(UpdateTrackingModel["Config"], BaseSettings):
|
||||
"""Config for the server."""
|
||||
|
||||
num_graph_workers: int = Field(
|
||||
default=1,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Maximum number of workers to use for graph execution.",
|
||||
)
|
||||
num_node_workers: int = Field(
|
||||
default=1,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Maximum number of workers to use for node execution within a single graph.",
|
||||
)
|
||||
pyro_host: str = Field(
|
||||
default="localhost",
|
||||
description="The default hostname of the Pyro server.",
|
||||
num_workers: int = Field(
|
||||
default=9, ge=1, le=100, description="Number of workers to use for execution."
|
||||
)
|
||||
# Add more configuration fields as needed
|
||||
|
||||
@@ -65,6 +52,7 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
|
||||
get_config_path() / "config.json",
|
||||
],
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
extra="allow",
|
||||
)
|
||||
|
||||
@@ -77,13 +65,7 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
|
||||
dotenv_settings: PydanticBaseSettingsSource,
|
||||
file_secret_settings: PydanticBaseSettingsSource,
|
||||
) -> Tuple[PydanticBaseSettingsSource, ...]:
|
||||
return (
|
||||
env_settings,
|
||||
file_secret_settings,
|
||||
dotenv_settings,
|
||||
JsonConfigSettingsSource(settings_cls),
|
||||
init_settings,
|
||||
)
|
||||
return (JsonConfigSettingsSource(settings_cls),)
|
||||
|
||||
|
||||
class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
|
||||
@@ -105,8 +87,6 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
|
||||
medium_api_key: str = Field(default="", description="Medium API key")
|
||||
medium_author_id: str = Field(default="", description="Medium author ID")
|
||||
|
||||
discord_bot_token: str = Field(default="", description="Discord bot token")
|
||||
|
||||
# Add more secret fields as needed
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
|
||||
@@ -13,7 +13,7 @@ log = print
|
||||
class SpinTestServer:
|
||||
def __init__(self):
|
||||
self.name_server = PyroNameServer()
|
||||
self.exec_manager = ExecutionManager()
|
||||
self.exec_manager = ExecutionManager(1)
|
||||
self.agent_server = AgentServer()
|
||||
self.scheduler = ExecutionScheduler()
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{
|
||||
"num_graph_workers": 10,
|
||||
"num_node_workers": 10
|
||||
}
|
||||
"num_workers": 5
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
directory = os.path.dirname(os.path.realpath(__file__))
|
||||
@@ -20,43 +19,9 @@ def lint():
|
||||
print("Lint failed, try running `poetry run format` to fix the issues: ", e)
|
||||
raise e
|
||||
|
||||
try:
|
||||
run("schema_lint")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("Lint failed, try running `poetry run schema` to fix the issues: ", e)
|
||||
raise e
|
||||
|
||||
|
||||
def format():
|
||||
run("ruff", "check", "--fix", ".")
|
||||
run("isort", "--profile", "black", ".")
|
||||
run("black", ".")
|
||||
run("pyright", ".")
|
||||
|
||||
|
||||
def schema():
|
||||
file = os.path.join(directory, "schema.prisma")
|
||||
text = open(file, "r").read()
|
||||
text = re.sub(r'provider\s+=\s+".*"', 'provider = "postgresql"', text, 1)
|
||||
text = re.sub(r'url\s+=\s+".*"', 'url = env("DATABASE_URL")', text, 1)
|
||||
text = "// THIS FILE IS AUTO-GENERATED, RUN `poetry run schema` TO UPDATE\n" + text
|
||||
with open(os.path.join(directory, "postgres", "schema.prisma"), "w") as f:
|
||||
f.write(text)
|
||||
|
||||
run("prisma", "format", "--schema", "schema.prisma")
|
||||
run("prisma", "format", "--schema", "postgres/schema.prisma")
|
||||
run("prisma", "migrate", "dev", "--schema", "schema.prisma")
|
||||
run("prisma", "migrate", "dev", "--schema", "postgres/schema.prisma")
|
||||
|
||||
|
||||
def schema_lint():
|
||||
def read_schema(path: str) -> list[str]:
|
||||
with open(path, "r") as f:
|
||||
return [v for v in f.read().splitlines() if not v.startswith("//")]
|
||||
|
||||
sqlite_schema = read_schema(os.path.join(directory, "schema.prisma"))
|
||||
postgres_schema = read_schema(os.path.join(directory, "postgres", "schema.prisma"))
|
||||
diff = [line.strip() for line in set(sqlite_schema) ^ set(postgres_schema)]
|
||||
|
||||
if line := next((v for v in diff if not v.startswith(("provider", "url"))), None):
|
||||
raise Exception(f"schema.prisma & postgres/schema.prisma mismatch: {line}")
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[referencedByInputExecId,referencedByOutputExecId,name]` on the table `AgentNodeExecutionInputOutput` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AgentNodeExecutionInputOutput_referencedByInputExecId_referencedByOutputExecId_name_key" ON "AgentNodeExecutionInputOutput"("referencedByInputExecId", "referencedByOutputExecId", "name");
|
||||
@@ -1,20 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "AgentNodeExecution" ADD COLUMN "executionData" TEXT;
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_AgentNodeLink" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"agentNodeSourceId" TEXT NOT NULL,
|
||||
"sourceName" TEXT NOT NULL,
|
||||
"agentNodeSinkId" TEXT NOT NULL,
|
||||
"sinkName" TEXT NOT NULL,
|
||||
"isStatic" BOOLEAN NOT NULL DEFAULT false,
|
||||
CONSTRAINT "AgentNodeLink_agentNodeSourceId_fkey" FOREIGN KEY ("agentNodeSourceId") REFERENCES "AgentNode" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "AgentNodeLink_agentNodeSinkId_fkey" FOREIGN KEY ("agentNodeSinkId") REFERENCES "AgentNode" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_AgentNodeLink" ("agentNodeSinkId", "agentNodeSourceId", "id", "sinkName", "sourceName") SELECT "agentNodeSinkId", "agentNodeSourceId", "id", "sinkName", "sourceName" FROM "AgentNodeLink";
|
||||
DROP TABLE "AgentNodeLink";
|
||||
ALTER TABLE "new_AgentNodeLink" RENAME TO "AgentNodeLink";
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "sqlite"
|
||||
provider = "sqlite"
|
||||
|
||||
79
rnd/autogpt_server/poetry.lock
generated
79
rnd/autogpt_server/poetry.lock
generated
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "agpt"
|
||||
@@ -25,7 +25,7 @@ requests = "*"
|
||||
sentry-sdk = "^1.40.4"
|
||||
|
||||
[package.extras]
|
||||
benchmark = ["agbenchmark @ file:///home/bently/Desktop/autogpt-ui/AutoGPT/benchmark"]
|
||||
benchmark = ["agbenchmark @ file:///workspaces/AutoGPT/benchmark"]
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
@@ -329,29 +329,12 @@ watchdog = "4.0.0"
|
||||
webdriver-manager = "^4.0.1"
|
||||
|
||||
[package.extras]
|
||||
benchmark = ["agbenchmark @ file:///home/bently/Desktop/autogpt-ui/AutoGPT/benchmark"]
|
||||
benchmark = ["agbenchmark @ file:///workspaces/AutoGPT/benchmark"]
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
url = "../../forge"
|
||||
|
||||
[[package]]
|
||||
name = "autogpt-libs"
|
||||
version = "0.1.0"
|
||||
description = "Shared libraries across NextGen AutoGPT"
|
||||
optional = false
|
||||
python-versions = ">=3.10,<4.0"
|
||||
files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
pyjwt = "^2.8.0"
|
||||
python-dotenv = "^1.0.1"
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
url = "../autogpt_libs"
|
||||
|
||||
[[package]]
|
||||
name = "backoff"
|
||||
version = "2.2.1"
|
||||
@@ -1073,26 +1056,6 @@ wrapt = ">=1.10,<2"
|
||||
[package.extras]
|
||||
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"]
|
||||
|
||||
[[package]]
|
||||
name = "discord-py"
|
||||
version = "2.4.0"
|
||||
description = "A Python wrapper for the Discord API"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "discord.py-2.4.0-py3-none-any.whl", hash = "sha256:b8af6711c70f7e62160bfbecb55be699b5cb69d007426759ab8ab06b1bd77d1d"},
|
||||
{file = "discord_py-2.4.0.tar.gz", hash = "sha256:d07cb2a223a185873a1d0ee78b9faa9597e45b3f6186df21a95cec1e9bcdc9a5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.7.4,<4"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (==4.4.0)", "sphinx-inline-tabs (==2023.4.21)", "sphinxcontrib-applehelp (==1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (==2.0.1)", "sphinxcontrib-jsmath (==1.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport (==1.2.4)", "typing-extensions (>=4.3,<5)"]
|
||||
speed = ["Brotli", "aiodns (>=1.1)", "cchardet (==2.1.7)", "orjson (>=3.5.4)"]
|
||||
test = ["coverage[toml]", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "typing-extensions (>=4.3,<5)", "tzdata"]
|
||||
voice = ["PyNaCl (>=1.3.0,<1.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
@@ -1216,20 +1179,6 @@ files = [
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "expiringdict"
|
||||
version = "1.2.2"
|
||||
description = "Dictionary with auto-expiring values for caching purposes"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "expiringdict-1.2.2-py3-none-any.whl", hash = "sha256:09a5d20bc361163e6432a874edd3179676e935eb81b925eccef48d409a8a45e8"},
|
||||
{file = "expiringdict-1.2.2.tar.gz", hash = "sha256:300fb92a7e98f15b05cf9a856c1415b3bc4f2e132be07daa326da6414c23ee09"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
tests = ["coverage", "coveralls", "dill", "mock", "nose"]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.109.2"
|
||||
@@ -2420,9 +2369,6 @@ files = [
|
||||
{file = "lief-0.14.1-cp312-cp312-manylinux_2_28_x86_64.manylinux_2_27_x86_64.whl", hash = "sha256:497b88f9c9aaae999766ba188744ee35c5f38b4b64016f7dbb7037e9bf325382"},
|
||||
{file = "lief-0.14.1-cp312-cp312-win32.whl", hash = "sha256:08bad88083f696915f8dcda4042a3bfc514e17462924ec8984085838b2261921"},
|
||||
{file = "lief-0.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:e131d6158a085f8a72124136816fefc29405c725cd3695ce22a904e471f0f815"},
|
||||
{file = "lief-0.14.1-cp313-cp313-manylinux_2_28_x86_64.manylinux_2_27_x86_64.whl", hash = "sha256:f9ff9a6959fb6d0e553cca41cd1027b609d27c5073e98d9fad8b774fbb5746c2"},
|
||||
{file = "lief-0.14.1-cp313-cp313-win32.whl", hash = "sha256:95f295a7cc68f4e14ce7ea4ff8082a04f5313c2e5e63cc2bbe9d059190b7e4d5"},
|
||||
{file = "lief-0.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:cdc1123c2e27970f8c8353505fd578e634ab33193c8d1dff36dc159e25599a40"},
|
||||
{file = "lief-0.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:df650fa05ca131e4dfeb42c77985e1eb239730af9944bc0aadb1dfac8576e0e8"},
|
||||
{file = "lief-0.14.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:b4e76eeb48ca2925c6ca6034d408582615f2faa855f9bb11482e7acbdecc4803"},
|
||||
{file = "lief-0.14.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:016e4fac91303466024154dd3c4b599e8b7c52882f72038b62a2be386d98c8f9"},
|
||||
@@ -4210,23 +4156,6 @@ files = [
|
||||
[package.extras]
|
||||
windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[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 = "pylatexenc"
|
||||
version = "2.10"
|
||||
@@ -6419,4 +6348,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "9991857e7076d3bfcbae7af6c2cec54dc943167a3adceb5a0ebf74d80c05778f"
|
||||
content-hash = "422edccf59dc5cdcc1cb0f6991228b2f58ef807a934d1aeda9643fa0cad27780"
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[referencedByInputExecId,referencedByOutputExecId,name]` on the table `AgentNodeExecutionInputOutput` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AgentNodeExecutionInputOutput_referencedByInputExecId_refer_key" ON "AgentNodeExecutionInputOutput"("referencedByInputExecId", "referencedByOutputExecId", "name");
|
||||
@@ -1,5 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "AgentNodeExecution" ADD COLUMN "executionData" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "AgentNodeLink" ADD COLUMN "isStatic" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -1,4 +1,3 @@
|
||||
// THIS FILE IS AUTO-GENERATED, RUN `poetry run schema` TO UPDATE
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
@@ -66,9 +65,6 @@ model AgentNodeLink {
|
||||
agentNodeSinkId String
|
||||
AgentNodeSink AgentNode @relation("AgentNodeSink", fields: [agentNodeSinkId], references: [id])
|
||||
sinkName String
|
||||
|
||||
// Default: the data coming from the source can only be consumed by the sink once, Static: input data will be reused.
|
||||
isStatic Boolean @default(false)
|
||||
}
|
||||
|
||||
// This model describes a component that will be executed by the AgentNode.
|
||||
@@ -112,8 +108,6 @@ model AgentNodeExecution {
|
||||
// sqlite does not support enum
|
||||
// enum Status { INCOMPLETE, QUEUED, RUNNING, SUCCESS, FAILED }
|
||||
executionStatus String
|
||||
// Final JSON serialized input data for the node execution.
|
||||
executionData String?
|
||||
addedTime DateTime @default(now())
|
||||
queuedTime DateTime?
|
||||
startedTime DateTime?
|
||||
@@ -133,9 +127,6 @@ model AgentNodeExecutionInputOutput {
|
||||
ReferencedByInputExec AgentNodeExecution? @relation("AgentNodeExecutionInput", fields: [referencedByInputExecId], references: [id])
|
||||
referencedByOutputExecId String?
|
||||
ReferencedByOutputExec AgentNodeExecution? @relation("AgentNodeExecutionOutput", fields: [referencedByOutputExecId], references: [id])
|
||||
|
||||
// Input and Output pin names are unique for each AgentNodeExecution.
|
||||
@@unique([referencedByInputExecId, referencedByOutputExecId, name])
|
||||
}
|
||||
|
||||
// This model describes the recurring execution schedule of an Agent.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user