Compare commits

...

21 Commits

Author SHA1 Message Date
abhi1992002
cc3fe03674 Improve spacing and typography in marketplace components 2025-04-04 11:21:08 +05:30
Nicholas Tindle
4760b2fdad fix(frontend): bad handling on error prompts (#9754)
<!-- Clearly explain the need for these changes: -->

I oopsed and had an extra unneeded parameter (as @majdyz pointed out)
and wasn't respected everywhere it was used.

### Changes 🏗️

<!-- Concisely describe all of the changes made in this pull request:
-->
- Remove parameter
- update all the places AuthFeedback is called

### Checklist 📋

#### For code changes:
- [ ] I have clearly listed my changes in the PR description
- [ ] I have made a test plan
- [ ] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
  - [ ] Test all pages with authfeedback on it

Co-authored-by: Bently <tomnoon9@gmail.com>
2025-04-04 11:20:09 +05:30
Reinier van der Leer
8389b583ff feat(frontend/library): Add "Open in builder" run action (#9755)
- Resolves #9730

### Changes 🏗️

- feat: Add "Open in builder" run action

- refactor: Add `ActionButtonGroup` to replace boilerplate code in
`AgentRunDetailsView`, `AgentRunDraftView`, `AgentScheduleDetailsView`
  - feat: Add link support to `ActionButtonGroup`, `ButtonAction`

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - Go to `/library/agents/[id]`
    - [x] "Run again" button works
    - [x] "Open in builder" button-link works
2025-04-04 11:20:09 +05:30
Bently
b782aba6b4 update(docs): Remove out dated tutorial video from docs & readme (#9753)
This is to remove the out dated tutorial video from docs & readme and
add a direct link to the docs in the readme

### Changes 🏗️

Remove video link from readme.md
Remove video link from
https://github.com/Significant-Gravitas/AutoGPT/blob/dev/docs/content/platform/getting-started.md
Add direct link to docs in readme.me
2025-04-04 11:20:09 +05:30
Reinier van der Leer
a1747d2521 feat(platform/library): Add real-time "Steps" count to agent run view (#9740)
- Resolves #9731

### Changes 🏗️

- feat: Add "Steps" showing `node_execution_count` to agent run view
  - Add `GraphExecutionMeta.stats.node_exec_count` attribute

- feat(backend/executor): Send graph execution update after *every* node
execution (instead of only I/O node executions)
  - Update graph execution stats after every node execution

- refactor: Move `GraphExecutionMeta` stats into sub-object
(`cost`, `duration`, `total_run_time` -> `stats.cost`, `stats.duration`,
`stats.node_exec_time`)

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - View an agent run with 1+ steps on `/library/agents/[id]`
    - [x] "Info" section layout doesn't break
    - [x] Number of steps is shown
  - Initiate a new agent run
    - [x] "Steps" increments in real time during execution
2025-04-04 11:20:09 +05:30
Madura Herath
7d83c5e57a docs(platform): Add WSL 2 recommendation for Docker on Windows (#9749)
In this pull request, the following changes have been made in response
to Issue #9190:

Documentation Changes:

- Added a note to the AutoGPT documentation regarding Docker
installation on Windows.

- Specifically, the note advises users to opt for WSL2 (Windows
Subsystem for Linux version 2) instead of Hyper-V during Docker setup to
prevent issues with Supabase, such as the "unhealthy" status for
supabase-db.

---------

Co-authored-by: Madura Herath <madurah@verdentra.com>
Co-authored-by: Bently <tomnoon9@gmail.com>
2025-04-04 11:20:09 +05:30
Reinier van der Leer
dac7f94928 fix(platform/library): Fix UX for webhook-triggered runs (#9680)
- Resolves #9679

### Changes 🏗️

Frontend:
- Fix crash on `payload` graph input
- Fix crash on object type agent I/O values
- Hide "+ New run" if `graph.webhook_id` is set

Backend:
- Add computed field `webhook_id` to `GraphModel`
  - Add computed property `webhook_input_node` to `GraphModel`
- Refactor:
  - Move `Node.webhook_id` -> `NodeModel.webhook_id`
  - Move `NodeModel.block` -> `Node.block` (computed property)
  - Replace `get_block(node.block_id)` with `node.block` where sensible

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Create and run a simple graph
  - [x] Create a graph with a webhook trigger and ensure it works
- [x] Check out the runs of a webhook-triggered graph and ensure the
page works
2025-04-04 11:20:09 +05:30
Nicholas Tindle
703809446c feat(backend, libs): Tell uvicorn to use our logger + always log to stdout+stderr (#9742)
<!-- Clearly explain the need for these changes: -->

Uvicorn and our logs were ending up in different places, this pr enures
uvicorn using our logging config, not their own.

### Changes 🏗️
- Clears uvicorn's loggers for rest, ws
- always log to stdout,stderr and additionally log to gcp is appropriate
<!-- Concisely describe all of the changes made in this pull request:
-->

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
- [x] Test all possible variants of the log cloud vs not and ensure that
uvicorn logs show up in the same place that rest of the system logs do
for all

---------

Co-authored-by: Zamil Majdy <zamil.majdy@agpt.co>
2025-04-04 11:20:09 +05:30
Reinier van der Leer
7fc5a57515 refactor(frontend): Clean up graph import & export logic (#9717)
- Resolves #9716
- Builds on the work done in #9627

### Changes 🏗️

- Remove `safeCopyGraph`; export directly from backend instead
- Explicitly name sanitization functions for *importing* graphs; move to
`@/lib/autogpt-server-api/utils`
- Amend `BackendAPI.getGraph(..)` to delete `.user_id` if `for_export ==
true`

Out-of-scope improvements:
- Add missing `user_id` to frontend `Graph` types
- Add `UserID` branded type for `User.id` + all `user_id` properties

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
- Create and configure an agent with the Publish To Medium block, a
block that uses credentials, and a webhook trigger
  - Go to `/monitoring` and click the agent you just created
    - [x] -> "Export" button should work
      - [x] -> Exported file contains no credentials or secrets
      - [x] -> Exported file contains no user IDs
      - [x] -> Exported file contains no webhook IDs
2025-04-04 11:20:09 +05:30
Abhimanyu Yadav
a35ade8585 fix(marketplace): Add 58px bottom padding to creator page agents section on large screens (#9738)
- fix #9000 

Currently, we have a 32px bottom padding on the Creator’s page on larger
screen. I have added an extra 58px to make it 90px.
2025-04-04 11:20:09 +05:30
Abhimanyu Yadav
cf36dcabc2 fix(marketplace): Fix margin between arrows and carousel (#9745)
- fix #8958 

Currently, the arrow button and carousel have a 16px margin, and the
button is placed 12px below the top of the container. This makes the
spacing appear to be 28px. Therefore, place the button and indicator at
the top of the container.
2025-04-04 11:19:52 +05:30
Abhimanyu Yadav
d59faa7628 fix(marketplace): Reduce margin between search bar and chips to 20px (#9748)
- fix #8955 

Reduce the margin between the search bar and chips from 24px to 20px.
2025-04-04 11:19:52 +05:30
Abhimanyu Yadav
1df9aef199 fix(marketplace): Fix store card typography (#9739)
- fix #8965 

### Changes Made:
- **Title**: Increased line height from 20px to 32px.
- **Creator Name:**
   - Changed font to Geist Sans.
   - Updated font size to 20px and leading to 28px.
- **Description**: Applied Geist Sans font.
- **Stats Line:** Applied Geist Sans font.
   - Font Configuration Fix:

> Previously, we were using font-gist, which is not defined in the
tailwind config file, hence Updated to use font-sans instead.

I have also fixed the height and width of the profile picture in the
creator card in this PR. The issue is linked below:
- #9314

![Screenshot 2025-04-02 at 6 32
10 PM](https://github.com/user-attachments/assets/1c2d9779-0a5e-4269-b3d2-37526a0949d3)

The margin is perfectly set to 24px; only the height and width of the
image need to be changed.

---------

Co-authored-by: Bently <tomnoon9@gmail.com>
2025-04-04 11:19:52 +05:30
Nicholas Tindle
c49aaf558a refactor(backend): move the router files for postmark to not the v2 folder (#9597)
<!-- Clearly explain the need for these changes: -->
One of the pull request review notes from when these were first made is
that they don't belong in the v2 folder. This pr fixes where they are.

### Changes 🏗️
- Moves from v2 to routers for the postmark tooling
<!-- Concisely describe all of the changes made in this pull request:
-->

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
  - [x] Check that linting and tests pass

---------

Co-authored-by: Zamil Majdy <zamil.majdy@agpt.co>
2025-04-04 11:19:30 +05:30
Nicholas Tindle
6a3e746544 fix(backend): handle notification service errors more elegently (#9734)
<!-- Clearly explain the need for these changes: -->
We have logged 272k timeout errors in the past week from the event loop.
Don't raise those as errors.

Also along the way for diagnosing this we found that some items were
inserted into batches with incomplete datasets so handle that too.

### Changes 🏗️
- Handle timeout errors explicitly 
- Add better messaging for other error types
- Add filtering for queueing bad mezsaging
- add filtering for reading bad batches
<!-- Concisely describe all of the changes made in this pull request:
-->

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
  - [x] Pull dev db
  - [x] Test new code to check stability + error reduction

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-04 11:19:30 +05:30
Reinier van der Leer
906988a945 fix(ci/backend): Use Poetry version from lockfile (#9729)
Currently, our CI always uses the latest version of Poetry. This causes
issues with the lockfile check whenever a new Poetry version is
released, especially if that new version has different lockfile
generation behavior.

This new mechanism determines the Poetry version to use as follows:
- Get Poetry version from backend/poetry.lock in the current branch
- Get Poetry version from backend/poetry.lock on the base branch
- Use the newest version out of the two found versions

This way, we don't automatically update to new Poetry versions, but it
is still possible to update to newer versions through pull requests.
2025-04-04 11:19:30 +05:30
Zamil Majdy
043fa551d9 fix(frontend): Fix toggle input label & time picker margin 2025-04-04 11:19:30 +05:30
Zamil Majdy
b09bd5d3b8 fix(frontend): Add border on opened select input-button 2025-04-04 11:19:30 +05:30
abhi1992002
248cd53dec Improve spacing and typography in marketplace components 2025-04-04 10:57:00 +05:30
abhi1992002
d9991832cb Add 60px margin bottom to AgentsSection component 2025-04-03 10:39:30 +05:30
abhi1992002
8f1b16aee9 Add bottom margin to Separators and improve section spacing 2025-04-03 09:49:11 +05:30
57 changed files with 618 additions and 528 deletions

View File

@@ -80,18 +80,35 @@ jobs:
- name: Install Poetry (Unix)
run: |
curl -sSL https://install.python-poetry.org | python3 -
# Extract Poetry version from backend/poetry.lock
HEAD_POETRY_VERSION=$(head -n 1 poetry.lock | grep -oP '(?<=Poetry )[0-9]+\.[0-9]+\.[0-9]+')
echo "Found Poetry version ${HEAD_POETRY_VERSION} in backend/poetry.lock"
if [ -n "$BASE_REF" ]; then
BASE_BRANCH=${BASE_REF/refs\/heads\//}
BASE_POETRY_VERSION=$((git show "origin/$BASE_BRANCH":./poetry.lock; true) | head -n 1 | grep -oP '(?<=Poetry )[0-9]+\.[0-9]+\.[0-9]+')
echo "Found Poetry version ${BASE_POETRY_VERSION} in backend/poetry.lock on ${BASE_REF}"
POETRY_VERSION=$(printf '%s\n' "$HEAD_POETRY_VERSION" "$BASE_POETRY_VERSION" | sort -V | tail -n1)
else
POETRY_VERSION=$HEAD_POETRY_VERSION
fi
echo "Using Poetry version ${POETRY_VERSION}"
# Install Poetry
curl -sSL https://install.python-poetry.org | POETRY_VERSION=$POETRY_VERSION python3 -
if [ "${{ runner.os }}" = "macOS" ]; then
PATH="$HOME/.local/bin:$PATH"
echo "$HOME/.local/bin" >> $GITHUB_PATH
fi
env:
BASE_REF: ${{ github.base_ref || github.event.merge_group.base_ref }}
- name: Check poetry.lock
run: |
poetry lock
if ! git diff --quiet poetry.lock; then
if ! git diff --quiet --ignore-matching-lines="^# " poetry.lock; then
echo "Error: poetry.lock not up to date."
echo
git diff poetry.lock

View File

@@ -15,7 +15,11 @@
> Setting up and hosting the AutoGPT Platform yourself is a technical process.
> If you'd rather something that just works, we recommend [joining the waitlist](https://bit.ly/3ZDijAI) for the cloud-hosted beta.
https://github.com/user-attachments/assets/d04273a5-b36a-4a37-818e-f631ce72d603
### Updated Setup Instructions:
Weve moved to a fully maintained and regularly updated documentation site.
👉 [Follow the official self-hosting guide here](https://docs.agpt.co/platform/getting-started/)
This tutorial assumes you have Docker, VSCode, git and npm installed.

View File

@@ -8,7 +8,7 @@ from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from .filters import BelowLevelFilter
from .formatters import AGPTFormatter, StructuredLoggingFormatter
from .formatters import AGPTFormatter
LOG_DIR = Path(__file__).parent.parent.parent.parent / "logs"
LOG_FILE = "activity.log"
@@ -81,9 +81,26 @@ def configure_logging(force_cloud_logging: bool = False) -> None:
"""
config = LoggingConfig()
log_handlers: list[logging.Handler] = []
# Console output handlers
stdout = logging.StreamHandler(stream=sys.stdout)
stdout.setLevel(config.level)
stdout.addFilter(BelowLevelFilter(logging.WARNING))
if config.level == logging.DEBUG:
stdout.setFormatter(AGPTFormatter(DEBUG_LOG_FORMAT))
else:
stdout.setFormatter(AGPTFormatter(SIMPLE_LOG_FORMAT))
stderr = logging.StreamHandler()
stderr.setLevel(logging.WARNING)
if config.level == logging.DEBUG:
stderr.setFormatter(AGPTFormatter(DEBUG_LOG_FORMAT))
else:
stderr.setFormatter(AGPTFormatter(SIMPLE_LOG_FORMAT))
log_handlers += [stdout, stderr]
# Cloud logging setup
if config.enable_cloud_logging or force_cloud_logging:
import google.cloud.logging
@@ -97,26 +114,7 @@ def configure_logging(force_cloud_logging: bool = False) -> None:
transport=SyncTransport,
)
cloud_handler.setLevel(config.level)
cloud_handler.setFormatter(StructuredLoggingFormatter())
log_handlers.append(cloud_handler)
else:
# Console output handlers
stdout = logging.StreamHandler(stream=sys.stdout)
stdout.setLevel(config.level)
stdout.addFilter(BelowLevelFilter(logging.WARNING))
if config.level == logging.DEBUG:
stdout.setFormatter(AGPTFormatter(DEBUG_LOG_FORMAT))
else:
stdout.setFormatter(AGPTFormatter(SIMPLE_LOG_FORMAT))
stderr = logging.StreamHandler()
stderr.setLevel(logging.WARNING)
if config.level == logging.DEBUG:
stderr.setFormatter(AGPTFormatter(DEBUG_LOG_FORMAT))
else:
stderr.setFormatter(AGPTFormatter(SIMPLE_LOG_FORMAT))
log_handlers += [stdout, stderr]
# File logging setup
if config.enable_file_logging:

View File

@@ -1,7 +1,6 @@
import logging
from colorama import Fore, Style
from google.cloud.logging_v2.handlers import CloudLoggingFilter, StructuredLogHandler
from .utils import remove_color_codes
@@ -80,16 +79,3 @@ class AGPTFormatter(FancyConsoleFormatter):
return remove_color_codes(super().format(record))
else:
return super().format(record)
class StructuredLoggingFormatter(StructuredLogHandler, logging.Formatter):
def __init__(self):
# Set up CloudLoggingFilter to add diagnostic info to the log records
self.cloud_logging_filter = CloudLoggingFilter()
# Init StructuredLogHandler
super().__init__()
def format(self, record: logging.LogRecord) -> str:
self.cloud_logging_filter.filter(record)
return super().format(record)

View File

@@ -1,7 +1,7 @@
import logging
import re
from typing import Any
import uvicorn.config
from colorama import Fore
@@ -25,3 +25,14 @@ def print_attribute(
"color": value_color,
},
)
def generate_uvicorn_config():
"""
Generates a uvicorn logging config that silences uvicorn's default logging and tells it to use the native logging module.
"""
log_config = dict(uvicorn.config.LOGGING_CONFIG)
log_config["loggers"]["uvicorn"] = {"handlers": []}
log_config["loggers"]["uvicorn.error"] = {"handlers": []}
log_config["loggers"]["uvicorn.access"] = {"handlers": []}
return log_config

View File

@@ -14,7 +14,6 @@ from backend.data.block import (
BlockOutput,
BlockSchema,
BlockType,
get_block,
)
from backend.data.model import SchemaField
from backend.util import json
@@ -264,9 +263,7 @@ class SmartDecisionMakerBlock(Block):
Raises:
ValueError: If the block specified by sink_node.block_id is not found.
"""
block = get_block(sink_node.block_id)
if not block:
raise ValueError(f"Block not found: {sink_node.block_id}")
block = sink_node.block
tool_function: dict[str, Any] = {
"name": re.sub(r"[^a-zA-Z0-9_-]", "_", block.name).lower(),

View File

@@ -59,23 +59,27 @@ ExecutionStatus = AgentExecutionStatus
class GraphExecutionMeta(BaseDbModel):
user_id: str
started_at: datetime
ended_at: datetime
cost: Optional[int] = Field(..., description="Execution cost in credits")
duration: float = Field(..., description="Seconds from start to end of run")
total_run_time: float = Field(..., description="Seconds of node runtime")
status: ExecutionStatus
graph_id: str
graph_version: int
preset_id: Optional[str] = None
status: ExecutionStatus
started_at: datetime
ended_at: datetime
class Stats(BaseModel):
cost: int = Field(..., description="Execution cost (cents)")
duration: float = Field(..., description="Seconds from start to end of run")
node_exec_time: float = Field(..., description="Seconds of total node runtime")
node_exec_count: int = Field(..., description="Number of node executions")
stats: Stats | None
@staticmethod
def from_db(_graph_exec: AgentGraphExecution):
now = datetime.now(timezone.utc)
# TODO: make started_at and ended_at optional
start_time = _graph_exec.startedAt or _graph_exec.createdAt
end_time = _graph_exec.updatedAt or now
duration = (end_time - start_time).total_seconds()
total_run_time = duration
try:
stats = GraphExecutionStats.model_validate(_graph_exec.stats)
@@ -87,21 +91,25 @@ class GraphExecutionMeta(BaseDbModel):
)
stats = None
duration = stats.walltime if stats else duration
total_run_time = stats.nodes_walltime if stats else total_run_time
return GraphExecutionMeta(
id=_graph_exec.id,
user_id=_graph_exec.userId,
started_at=start_time,
ended_at=end_time,
cost=stats.cost if stats else None,
duration=duration,
total_run_time=total_run_time,
status=ExecutionStatus(_graph_exec.executionStatus),
graph_id=_graph_exec.agentGraphId,
graph_version=_graph_exec.agentGraphVersion,
preset_id=_graph_exec.agentPresetId,
status=ExecutionStatus(_graph_exec.executionStatus),
started_at=start_time,
ended_at=end_time,
stats=(
GraphExecutionMeta.Stats(
cost=stats.cost,
duration=stats.walltime,
node_exec_time=stats.nodes_walltime,
node_exec_count=stats.node_count,
)
if stats
else None
),
)
@@ -116,10 +124,11 @@ class GraphExecution(GraphExecutionMeta):
graph_exec = GraphExecutionMeta.from_db(_graph_exec)
node_executions = sorted(
complete_node_executions = sorted(
[
NodeExecutionResult.from_db(ne, _graph_exec.userId)
for ne in _graph_exec.AgentNodeExecutions
if ne.executionStatus != ExecutionStatus.INCOMPLETE
],
key=lambda ne: (ne.queue_time is None, ne.queue_time or ne.add_time),
)
@@ -128,7 +137,7 @@ class GraphExecution(GraphExecutionMeta):
**{
# inputs from Agent Input Blocks
exec.input_data["name"]: exec.input_data.get("value")
for exec in node_executions
for exec in complete_node_executions
if (
(block := get_block(exec.block_id))
and block.block_type == BlockType.INPUT
@@ -137,7 +146,7 @@ class GraphExecution(GraphExecutionMeta):
**{
# input from webhook-triggered block
"payload": exec.input_data["payload"]
for exec in node_executions
for exec in complete_node_executions
if (
(block := get_block(exec.block_id))
and block.block_type
@@ -147,7 +156,7 @@ class GraphExecution(GraphExecutionMeta):
}
outputs: CompletedBlockOutput = defaultdict(list)
for exec in node_executions:
for exec in complete_node_executions:
if (
block := get_block(exec.block_id)
) and block.block_type == BlockType.OUTPUT:

View File

@@ -53,22 +53,23 @@ class Node(BaseDbModel):
input_links: list[Link] = []
output_links: list[Link] = []
webhook_id: Optional[str] = None
@property
def block(self) -> Block[BlockSchema, BlockSchema]:
block = get_block(self.block_id)
if not block:
raise ValueError(
f"Block #{self.block_id} does not exist -> Node #{self.id} is invalid"
)
return block
class NodeModel(Node):
graph_id: str
graph_version: int
webhook_id: Optional[str] = None
webhook: Optional[Webhook] = None
@property
def block(self) -> Block[BlockSchema, BlockSchema]:
block = get_block(self.block_id)
if not block:
raise ValueError(f"Block #{self.block_id} does not exist")
return block
@staticmethod
def from_db(node: AgentNode, for_export: bool = False) -> "NodeModel":
obj = NodeModel(
@@ -88,8 +89,7 @@ class NodeModel(Node):
return obj
def is_triggered_by_event_type(self, event_type: str) -> bool:
if not (block := get_block(self.block_id)):
raise ValueError(f"Block #{self.block_id} not found for node #{self.id}")
block = self.block
if not block.webhook_config:
raise TypeError("This method can't be used on non-webhook blocks")
if not block.webhook_config.event_filter_input:
@@ -166,11 +166,10 @@ class BaseGraph(BaseDbModel):
def input_schema(self) -> dict[str, Any]:
return self._generate_schema(
*(
(b.input_schema, node.input_default)
(block.input_schema, node.input_default)
for node in self.nodes
if (b := get_block(node.block_id))
and b.block_type == BlockType.INPUT
and issubclass(b.input_schema, AgentInputBlock.Input)
if (block := node.block).block_type == BlockType.INPUT
and issubclass(block.input_schema, AgentInputBlock.Input)
)
)
@@ -179,11 +178,10 @@ class BaseGraph(BaseDbModel):
def output_schema(self) -> dict[str, Any]:
return self._generate_schema(
*(
(b.input_schema, node.input_default)
(block.input_schema, node.input_default)
for node in self.nodes
if (b := get_block(node.block_id))
and b.block_type == BlockType.OUTPUT
and issubclass(b.input_schema, AgentOutputBlock.Input)
if (block := node.block).block_type == BlockType.OUTPUT
and issubclass(block.input_schema, AgentOutputBlock.Input)
)
)
@@ -228,13 +226,16 @@ class GraphModel(Graph):
user_id: str
nodes: list[NodeModel] = [] # type: ignore
@computed_field
@property
def starting_nodes(self) -> list[Node]:
def has_webhook_trigger(self) -> bool:
return self.webhook_input_node is not None
@property
def starting_nodes(self) -> list[NodeModel]:
outbound_nodes = {link.sink_id for link in self.links}
input_nodes = {
v.id
for v in self.nodes
if (b := get_block(v.block_id)) and b.block_type == BlockType.INPUT
node.id for node in self.nodes if node.block.block_type == BlockType.INPUT
}
return [
node
@@ -242,6 +243,18 @@ class GraphModel(Graph):
if node.id not in outbound_nodes or node.id in input_nodes
]
@property
def webhook_input_node(self) -> NodeModel | None:
return next(
(
node
for node in self.nodes
if node.block.block_type
in (BlockType.WEBHOOK, BlockType.WEBHOOK_MANUAL)
),
None,
)
def reassign_ids(self, user_id: str, reassign_graph_id: bool = False):
"""
Reassigns all IDs in the graph to new UUIDs.
@@ -391,9 +404,7 @@ class GraphModel(Graph):
node_map = {v.id: v for v in graph.nodes}
def is_static_output_block(nid: str) -> bool:
bid = node_map[nid].block_id
b = get_block(bid)
return b.static_output if b else False
return node_map[nid].block.static_output
# Links: links are connected and the connected pin data type are compatible.
for link in graph.links:
@@ -747,7 +758,6 @@ async def __create_graph(tx, graph: Graph, user_id: str):
"agentBlockId": node.block_id,
"constantInput": Json(node.input_default),
"metadata": Json(node.metadata),
"webhookId": node.webhook_id,
}
for graph in graphs
for node in graph.nodes

View File

@@ -1,4 +1,5 @@
import prisma
import prisma.enums
import prisma.types
from backend.blocks.io import IO_BLOCK_IDs
@@ -46,6 +47,9 @@ GRAPH_EXECUTION_INCLUDE: prisma.types.AgentGraphExecutionInclude = {
"AgentNode": {
"AgentBlock": {"id": {"in": IO_BLOCK_IDs}}, # type: ignore
},
"NOT": {
"executionStatus": prisma.enums.AgentExecutionStatus.INCOMPLETE,
},
},
}
}

View File

@@ -398,6 +398,8 @@ async def create_or_add_to_user_notification_batch(
logger.info(
f"Creating or adding to notification batch for {user_id} with type {notification_type} and data {notification_data}"
)
if not notification_data.data:
raise ValueError("Notification data must be provided")
# Serialize the data
json_data: Json = Json(notification_data.data.model_dump())

View File

@@ -157,18 +157,11 @@ def execute_node(
node = db_client.get_node(node_id)
node_block = get_block(node.block_id)
if not node_block:
logger.error(f"Block {node.block_id} not found.")
return
node_block = node.block
def push_output(output_name: str, output_data: Any) -> None:
_push_node_execution_output(
db_client=db_client,
user_id=user_id,
graph_exec_id=graph_exec_id,
db_client.upsert_execution_output(
node_exec_id=node_exec_id,
block_id=node_block.id,
output_name=output_name,
output_data=output_data,
)
@@ -280,35 +273,6 @@ def execute_node(
execution_stats.output_size = output_size
def _push_node_execution_output(
db_client: "DatabaseManager",
user_id: str,
graph_exec_id: str,
node_exec_id: str,
block_id: str,
output_name: str,
output_data: Any,
):
from backend.blocks.io import IO_BLOCK_IDs
db_client.upsert_execution_output(
node_exec_id=node_exec_id,
output_name=output_name,
output_data=output_data,
)
# Automatically push execution updates for all agent I/O
if block_id in IO_BLOCK_IDs:
graph_exec = db_client.get_graph_execution(
user_id=user_id, execution_id=graph_exec_id
)
if not graph_exec:
raise ValueError(
f"Graph execution #{graph_exec_id} for user #{user_id} not found"
)
db_client.send_execution_update(graph_exec)
def _enqueue_next_nodes(
db_client: "DatabaseManager",
node: Node,
@@ -748,15 +712,19 @@ class Executor:
Exception | None: The error that occurred during the execution, if any.
"""
log_metadata.info(f"Start graph execution {graph_exec.graph_exec_id}")
exec_stats = GraphExecutionStats()
execution_stats = GraphExecutionStats()
execution_status = ExecutionStatus.RUNNING
error = None
finished = False
def cancel_handler():
nonlocal execution_status
while not cancel.is_set():
cancel.wait(1)
if finished:
return
execution_status = ExecutionStatus.TERMINATED
cls.executor.terminate()
log_metadata.info(f"Terminated graph execution {graph_exec.graph_exec_id}")
cls._init_node_executor_pool()
@@ -779,18 +747,34 @@ class Executor:
if not isinstance(result, NodeExecutionStats):
return
nonlocal exec_stats
exec_stats.node_count += 1
exec_stats.nodes_cputime += result.cputime
exec_stats.nodes_walltime += result.walltime
nonlocal execution_stats
execution_stats.node_count += 1
execution_stats.nodes_cputime += result.cputime
execution_stats.nodes_walltime += result.walltime
if (err := result.error) and isinstance(err, Exception):
exec_stats.node_error_count += 1
execution_stats.node_error_count += 1
if _graph_exec := cls.db_client.update_graph_execution_stats(
graph_exec_id=exec_data.graph_exec_id,
status=execution_status,
stats=execution_stats,
):
cls.db_client.send_execution_update(_graph_exec)
else:
logger.error(
"Callback for "
f"finished node execution #{exec_data.node_exec_id} "
"could not update execution stats "
f"for graph execution #{exec_data.graph_exec_id}; "
f"triggered while graph exec status = {execution_status}"
)
return callback
while not queue.empty():
if cancel.is_set():
return exec_stats, ExecutionStatus.TERMINATED, error
execution_status = ExecutionStatus.TERMINATED
return execution_stats, execution_status, error
exec_data = queue.get()
@@ -812,29 +796,26 @@ class Executor:
exec_cost_counter = cls._charge_usage(
node_exec=exec_data,
execution_count=exec_cost_counter + 1,
execution_stats=exec_stats,
execution_stats=execution_stats,
)
except InsufficientBalanceError as error:
node_exec_id = exec_data.node_exec_id
_push_node_execution_output(
db_client=cls.db_client,
user_id=graph_exec.user_id,
graph_exec_id=graph_exec.graph_exec_id,
cls.db_client.upsert_execution_output(
node_exec_id=node_exec_id,
block_id=exec_data.block_id,
output_name="error",
output_data=str(error),
)
execution_status = ExecutionStatus.FAILED
exec_update = cls.db_client.update_node_execution_status(
node_exec_id, ExecutionStatus.FAILED
node_exec_id, execution_status
)
cls.db_client.send_execution_update(exec_update)
cls._handle_low_balance_notif(
graph_exec.user_id,
graph_exec.graph_id,
exec_stats,
execution_stats,
error,
)
raise
@@ -852,7 +833,8 @@ class Executor:
)
for node_id, execution in list(running_executions.items()):
if cancel.is_set():
return exec_stats, ExecutionStatus.TERMINATED, error
execution_status = ExecutionStatus.TERMINATED
return execution_stats, execution_status, error
if not queue.empty():
break # yield to parent loop to execute new queue items
@@ -879,7 +861,7 @@ class Executor:
cancel_thread.join()
clean_exec_files(graph_exec.graph_exec_id)
return exec_stats, execution_status, error
return execution_stats, execution_status, error
@classmethod
def _handle_agent_run_notif(
@@ -1016,10 +998,10 @@ class ExecutionManager(AppService):
nodes_input = []
for node in graph.starting_nodes:
input_data = {}
block = get_block(node.block_id)
block = node.block
# Invalid block & Note block should never be executed.
if not block or block.block_type == BlockType.NOTE:
# Note block should never be executed.
if block.block_type == BlockType.NOTE:
continue
# Extract request input data, and assign it to the input pin.
@@ -1127,9 +1109,7 @@ class ExecutionManager(AppService):
"""Checks all credentials for all nodes of the graph"""
for node in graph.nodes:
block = get_block(node.block_id)
if not block:
raise ValueError(f"Unknown block {node.block_id} for node #{node.id}")
block = node.block
# Find any fields of type CredentialsMetaInput
credentials_fields = cast(

View File

@@ -1,7 +1,7 @@
import logging
from typing import TYPE_CHECKING, Callable, Optional, cast
from backend.data.block import BlockSchema, BlockWebhookConfig, get_block
from backend.data.block import BlockSchema, BlockWebhookConfig
from backend.data.graph import set_node_webhook
from backend.integrations.webhooks import get_webhook_manager, supports_webhooks
@@ -29,12 +29,7 @@ async def on_graph_activate(
# Compare nodes in new_graph_version with previous_graph_version
updated_nodes = []
for new_node in graph.nodes:
block = get_block(new_node.block_id)
if not block:
raise ValueError(
f"Node #{new_node.id} is instance of unknown block #{new_node.block_id}"
)
block_input_schema = cast(BlockSchema, block.input_schema)
block_input_schema = cast(BlockSchema, new_node.block.input_schema)
node_credentials = None
if (
@@ -75,12 +70,7 @@ async def on_graph_deactivate(
"""
updated_nodes = []
for node in graph.nodes:
block = get_block(node.block_id)
if not block:
raise ValueError(
f"Node #{node.id} is instance of unknown block #{node.block_id}"
)
block_input_schema = cast(BlockSchema, block.input_schema)
block_input_schema = cast(BlockSchema, node.block.input_schema)
node_credentials = None
if (
@@ -113,11 +103,7 @@ async def on_node_activate(
) -> "NodeModel":
"""Hook to be called when the node is activated/created"""
block = get_block(node.block_id)
if not block:
raise ValueError(
f"Node #{node.id} is instance of unknown block #{node.block_id}"
)
block = node.block
if not block.webhook_config:
return node
@@ -224,11 +210,7 @@ async def on_node_deactivate(
"""Hook to be called when node is deactivated/deleted"""
logger.debug(f"Deactivating node #{node.id}")
block = get_block(node.block_id)
if not block:
raise ValueError(
f"Node #{node.id} is instance of unknown block #{node.block_id}"
)
block = node.block
if not block.webhook_config:
return node

View File

@@ -245,20 +245,26 @@ class NotificationManager(AppService):
continue
unsub_link = generate_unsubscribe_link(batch.user_id)
events = [
NotificationEventModel[
get_notif_data_type(db_event.type)
].model_validate(
{
"user_id": batch.user_id,
"type": db_event.type,
"data": db_event.data,
"created_at": db_event.created_at,
}
)
for db_event in batch_data.notifications
]
events = []
for db_event in batch_data.notifications:
try:
events.append(
NotificationEventModel[
get_notif_data_type(db_event.type)
].model_validate(
{
"user_id": batch.user_id,
"type": db_event.type,
"data": db_event.data,
"created_at": db_event.created_at,
}
)
)
except Exception as e:
logger.error(
f"Error parsing notification event: {e=}, {db_event=}"
)
continue
logger.info(f"{events=}")
self.email_sender.send_templated(
@@ -668,6 +674,8 @@ class NotificationManager(AppService):
except QueueEmpty:
logger.debug(f"Queue {error_queue_name} empty")
except TimeoutError:
logger.debug(f"Queue {error_queue_name} timed out")
except Exception as e:
if message:
logger.error(
@@ -675,8 +683,8 @@ class NotificationManager(AppService):
)
self.run_and_wait(message.reject(requeue=False))
else:
logger.error(
f"Error in notification service loop, message unable to be rejected, and will have to be manually removed to free space in the queue: {e}"
logger.exception(
f"Error in notification service loop, message unable to be rejected, and will have to be manually removed to free space in the queue: {e=}"
)
def run_service(self):

View File

@@ -3,6 +3,7 @@ import logging
from typing import Any, Optional
import autogpt_libs.auth.models
from autogpt_libs.logging.utils import generate_uvicorn_config
import fastapi
import fastapi.responses
import starlette.middleware.cors
@@ -17,13 +18,13 @@ import backend.data.db
import backend.data.graph
import backend.data.user
import backend.server.integrations.router
import backend.server.routers.postmark.postmark
import backend.server.routers.v1
import backend.server.v2.admin.store_admin_routes
import backend.server.v2.library.db
import backend.server.v2.library.model
import backend.server.v2.library.routes
import backend.server.v2.otto.routes
import backend.server.v2.postmark.postmark
import backend.server.v2.store.model
import backend.server.v2.store.routes
import backend.util.service
@@ -115,8 +116,8 @@ app.include_router(
)
app.include_router(
backend.server.v2.postmark.postmark.router,
tags=["v2", "email"],
backend.server.routers.postmark.postmark.router,
tags=["v1", "email"],
prefix="/api/email",
)
@@ -141,6 +142,7 @@ class AgentServer(backend.util.service.AppProcess):
server_app,
host=backend.util.settings.Config().agent_api_host,
port=backend.util.settings.Config().agent_api_port,
log_config=generate_uvicorn_config(),
)
@staticmethod

View File

@@ -10,7 +10,7 @@ from backend.data.user import (
set_user_email_verification,
unsubscribe_user_by_token,
)
from backend.server.v2.postmark.models import (
from backend.server.routers.postmark.models import (
PostmarkBounceEnum,
PostmarkBounceWebhook,
PostmarkClickWebhook,

View File

@@ -3,6 +3,7 @@ import logging
from contextlib import asynccontextmanager
from typing import Protocol
from autogpt_libs.logging.utils import generate_uvicorn_config
import uvicorn
from autogpt_libs.auth import parse_jwt_token
from autogpt_libs.utils.cache import thread_cached
@@ -286,8 +287,10 @@ class WebsocketServer(AppProcess):
allow_methods=["*"],
allow_headers=["*"],
)
uvicorn.run(
server_app,
host=Config().websocket_server_host,
port=Config().websocket_server_port,
log_config=generate_uvicorn_config(),
)

View File

@@ -89,11 +89,14 @@ async def test_send_graph_execution_result(
graph_id="test_graph",
graph_version=1,
status=ExecutionStatus.COMPLETED,
cost=0,
duration=1.2,
total_run_time=0.5,
started_at=datetime.now(tz=timezone.utc),
ended_at=datetime.now(tz=timezone.utc),
stats=GraphExecutionEvent.Stats(
cost=0,
duration=1.2,
node_exec_time=0.5,
node_exec_count=2,
),
inputs={
"input_1": "some input value :)",
"input_2": "some *other* input value",

View File

@@ -74,7 +74,7 @@
@layer components {
.agpt-border-input {
@apply m-0.5 border border-input focus-visible:border-gray-400 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-400;
@apply m-0.5 border border-input focus:border-gray-400 focus:outline-none focus:ring-1 focus:ring-gray-400 data-[state=open]:border-gray-400 data-[state=open]:ring-1 data-[state=open]:ring-gray-400;
}
.agpt-shadow-input {

View File

@@ -8,8 +8,8 @@ import {
GraphExecution,
GraphExecutionID,
GraphExecutionMeta,
Graph,
GraphID,
GraphMeta,
LibraryAgent,
LibraryAgentID,
Schedule,
@@ -30,7 +30,7 @@ export default function AgentRunsPage(): React.ReactElement {
// ============================ STATE =============================
const [graph, setGraph] = useState<GraphMeta | null>(null);
const [graph, setGraph] = useState<Graph | null>(null);
const [agent, setAgent] = useState<LibraryAgent | null>(null);
const [agentRuns, setAgentRuns] = useState<GraphExecutionMeta[]>([]);
const [schedules, setSchedules] = useState<Schedule[]>([]);
@@ -63,9 +63,7 @@ export default function AgentRunsPage(): React.ReactElement {
setSelectedSchedule(schedule);
}, []);
const [graphVersions, setGraphVersions] = useState<Record<number, GraphMeta>>(
{},
);
const [graphVersions, setGraphVersions] = useState<Record<number, Graph>>({});
const getGraphVersion = useCallback(
async (graphID: GraphID, version: number) => {
if (graphVersions[version]) return graphVersions[version];
@@ -228,12 +226,8 @@ export default function AgentRunsPage(): React.ReactElement {
...(agent?.can_access_graph
? [
{
label: "Open in builder",
callback: () =>
agent &&
router.push(
`/build?flowID=${agent.agent_id}&flowVersion=${agent.agent_version}`,
),
label: "Open graph in builder",
href: `/build?flowID=${agent.agent_id}&flowVersion=${agent.agent_version}`,
},
{ label: "Export agent to file", callback: downloadGraph },
]
@@ -244,7 +238,7 @@ export default function AgentRunsPage(): React.ReactElement {
callback: () => setAgentDeleteDialogOpen(true),
},
],
[agent, router, downloadGraph],
[agent, downloadGraph],
);
if (!agent || !graph) {
@@ -262,6 +256,7 @@ export default function AgentRunsPage(): React.ReactElement {
agentRuns={agentRuns}
schedules={schedules}
selectedView={selectedView}
allowDraftNewRun={!graph.has_webhook_trigger}
onSelectRun={selectRun}
onSelectSchedule={selectSchedule}
onSelectDraftNewRun={openRunDraftView}
@@ -283,6 +278,7 @@ export default function AgentRunsPage(): React.ReactElement {
{(selectedView.type == "run" && selectedView.id ? (
selectedRun && (
<AgentRunDetailsView
agent={agent}
graph={graphVersions[selectedRun.graph_version] ?? graph}
run={selectedRun}
agentActions={agentActions}

View File

@@ -26,6 +26,7 @@ import {
PasswordInput,
} from "@/components/auth";
import { loginFormSchema } from "@/types/auth";
import { getBehaveAs } from "@/lib/utils";
export default function LoginPage() {
const { supabase, user, isUserLoading } = useSupabase();
@@ -147,7 +148,11 @@ export default function LoginPage() {
Login
</AuthButton>
</form>
<AuthFeedback message={feedback} isError={true} />
<AuthFeedback
message={feedback}
isError={!!feedback}
behaveAs={getBehaveAs()}
/>
</Form>
<AuthBottomText
text="Don't have an account?"

View File

@@ -81,16 +81,17 @@ export default async function Page({
}
/>
</div>
<Separator className="mb-[25px] mt-6" />
<Separator className="mb-[25px] mt-7" />
<AgentsSection
agents={otherAgents.agents}
sectionTitle={`Other agents by ${agent.creator}`}
/>
<Separator className="mb-[25px] mt-6" />
<Separator className="mb-[25px] mt-[60px]" />
<AgentsSection
agents={similarAgents.agents}
sectionTitle="Similar agents"
/>
<Separator className="mb-[25px] mt-[60px]" />
<BecomeACreator
title="Become a Creator"
description="Join our ever-growing community of hackers and tinkerers"

View File

@@ -77,7 +77,7 @@ export default async function Page({
<CreatorLinks links={creator.links} />
</div>
</div>
<div className="mt-8 sm:mt-12 md:mt-16">
<div className="mt-8 sm:mt-12 md:mt-16 lg:pb-[58px]">
<hr className="w-full bg-neutral-700" />
<AgentsSection
agents={creatorAgents.agents}

View File

@@ -153,16 +153,16 @@ export default async function Page({}: {}) {
<main className="px-4">
<HeroSection />
<FeaturedSection featuredAgents={featuredAgents.agents} />
<Separator />
<Separator className="mb-[25px] mt-[100px]" />
<AgentsSection
sectionTitle="Top Agents"
agents={topAgents.agents as Agent[]}
/>
<Separator />
<Separator className="mb-[25px] mt-[60px]" />
<FeaturedCreators
featuredCreators={featuredCreators.creators as FeaturedCreator[]}
/>
<Separator />
<Separator className="mb-[25px] mt-[60px]" />
<BecomeACreator
title="Become a Creator"
description="Join our ever-growing community of hackers and tinkerers"

View File

@@ -24,6 +24,7 @@ import { useForm } from "react-hook-form";
import { z } from "zod";
import { changePassword, sendResetEmail } from "./actions";
import Spinner from "@/components/Spinner";
import { getBehaveAs } from "@/lib/utils";
export default function ResetPasswordPage() {
const { supabase, user, isUserLoading } = useSupabase();
@@ -151,7 +152,11 @@ export default function ResetPasswordPage() {
>
Update password
</AuthButton>
<AuthFeedback message={feedback} isError={isError} />
<AuthFeedback
message={feedback}
isError={isError}
behaveAs={getBehaveAs()}
/>
</Form>
</form>
) : (
@@ -178,7 +183,11 @@ export default function ResetPasswordPage() {
>
Send reset email
</AuthButton>
<AuthFeedback message={feedback} isError={isError} />
<AuthFeedback
message={feedback}
isError={isError}
behaveAs={getBehaveAs()}
/>
</Form>
</form>
)}

View File

@@ -36,7 +36,6 @@ export default function SignupPage() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
//TODO: Remove after closed beta
const [showErrorPrompt, setShowErrorPrompt] = useState(false);
const form = useForm<z.infer<typeof signupFormSchema>>({
resolver: zodResolver(signupFormSchema),
@@ -64,13 +63,11 @@ export default function SignupPage() {
setFeedback("User with this email already exists");
return;
} else {
setShowErrorPrompt(true);
setFeedback(error);
}
return;
}
setFeedback(null);
setShowErrorPrompt(false);
},
[form],
);
@@ -193,7 +190,6 @@ export default function SignupPage() {
<AuthFeedback
message={feedback}
isError={!!feedback}
showErrorPrompt={showErrorPrompt}
behaveAs={getBehaveAs()}
/>

View File

@@ -1,4 +1,5 @@
import { z } from "zod";
import { cn } from "@/lib/utils";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useState } from "react";
@@ -12,12 +13,14 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { Graph, GraphCreatable } from "@/lib/autogpt-server-api";
import { cn, removeCredentials } from "@/lib/utils";
import { EnterIcon } from "@radix-ui/react-icons";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import {
Graph,
GraphCreatable,
sanitizeImportedGraph,
} from "@/lib/autogpt-server-api";
// Add this custom schema for File type
const fileSchema = z.custom<File>((val) => val instanceof File, {
@@ -31,45 +34,6 @@ const formSchema = z.object({
importAsTemplate: z.boolean(),
});
export const updatedBlockIDMap: Record<string, string> = {
// https://github.com/Significant-Gravitas/AutoGPT/issues/8223
"a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6":
"436c3984-57fd-4b85-8e9a-459b356883bd",
"b2g2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6":
"0e50422c-6dee-4145-83d6-3a5a392f65de",
"c3d4e5f6-7g8h-9i0j-1k2l-m3n4o5p6q7r8":
"a0a69be1-4528-491c-a85a-a4ab6873e3f0",
"c3d4e5f6-g7h8-i9j0-k1l2-m3n4o5p6q7r8":
"32a87eab-381e-4dd4-bdb8-4c47151be35a",
"b2c3d4e5-6f7g-8h9i-0j1k-l2m3n4o5p6q7":
"87840993-2053-44b7-8da4-187ad4ee518c",
"h1i2j3k4-5l6m-7n8o-9p0q-r1s2t3u4v5w6":
"d0822ab5-9f8a-44a3-8971-531dd0178b6b",
"d3f4g5h6-1i2j-3k4l-5m6n-7o8p9q0r1s2t":
"df06086a-d5ac-4abb-9996-2ad0acb2eff7",
"h5e7f8g9-1b2c-3d4e-5f6g-7h8i9j0k1l2m":
"f5b0f5d0-1862-4d61-94be-3ad0fa772760",
"a1234567-89ab-cdef-0123-456789abcdef":
"4335878a-394e-4e67-adf2-919877ff49ae",
"f8e7d6c5-b4a3-2c1d-0e9f-8g7h6i5j4k3l":
"f66a3543-28d3-4ab5-8945-9b336371e2ce",
"b29c1b50-5d0e-4d9f-8f9d-1b0e6fcbf0h2":
"716a67b3-6760-42e7-86dc-18645c6e00fc",
"31d1064e-7446-4693-o7d4-65e5ca9110d1":
"cc10ff7b-7753-4ff2-9af6-9399b1a7eddc",
"c6731acb-4105-4zp1-bc9b-03d0036h370g":
"5ebe6768-8e5d-41e3-9134-1c7bd89a8d52",
};
function updateBlockIDs(graph: Graph) {
graph.nodes
.filter((node) => node.block_id in updatedBlockIDMap)
.forEach((node) => {
node.block_id = updatedBlockIDMap[node.block_id];
});
return graph;
}
export const AgentImportForm: React.FC<
React.FormHTMLAttributes<HTMLFormElement>
> = ({ className, ...props }) => {
@@ -150,12 +114,11 @@ export const AgentImportForm: React.FC<
JSON.stringify(obj, null, 2),
);
}
const agent = obj as Graph;
removeCredentials(agent);
updateBlockIDs(agent);
setAgentObject(agent);
form.setValue("agentName", agent.name);
form.setValue("agentDescription", agent.description);
const graph = obj as Graph;
sanitizeImportedGraph(graph);
setAgentObject(graph);
form.setValue("agentName", graph.name);
form.setValue("agentDescription", graph.description);
} catch (error) {
console.error("Error loading agent file:", error);
}

View File

@@ -4,17 +4,18 @@ import moment from "moment";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import {
Graph,
GraphExecution,
GraphExecutionID,
GraphExecutionMeta,
GraphMeta,
LibraryAgent,
} from "@/lib/autogpt-server-api";
import type { ButtonAction } from "@/components/agptui/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { IconRefresh, IconSquare } from "@/components/ui/icons";
import { useToastOnFail } from "@/components/ui/use-toast";
import { Button } from "@/components/agptui/Button";
import ActionButtonGroup from "@/components/agptui/action-button-group";
import { Input } from "@/components/ui/input";
import {
@@ -23,13 +24,15 @@ import {
} from "@/components/agents/agent-run-status-chip";
export default function AgentRunDetailsView({
agent,
graph,
run,
agentActions,
onRun,
deleteRun,
}: {
graph: GraphMeta;
agent: LibraryAgent;
graph: Graph;
run: GraphExecution | GraphExecutionMeta;
agentActions: ButtonAction[];
onRun: (runID: GraphExecutionID) => void;
@@ -55,16 +58,28 @@ export default function AgentRunDetailsView({
label: "Started",
value: `${moment(run.started_at).fromNow()}, ${moment(run.started_at).format("HH:mm")}`,
},
{
label: "Duration",
value: moment.duration(run.duration, "seconds").humanize(),
},
...(run.cost ? [{ label: "Cost", value: `${run.cost} credits` }] : []),
...(run.stats
? [
{
label: "Duration",
value: moment.duration(run.stats.duration, "seconds").humanize(),
},
{ label: "Steps", value: run.stats.node_exec_count },
{ label: "Cost", value: `${run.stats.cost} credits` },
]
: []),
];
}, [run, runStatus]);
const agentRunInputs:
| Record<string, { title?: string; /* type: BlockIOSubType; */ value: any }>
| Record<
string,
{
title?: string;
/* type: BlockIOSubType; */
value: string | number | undefined;
}
>
| undefined = useMemo(() => {
if (!("inputs" in run)) return undefined;
// TODO: show (link to) preset - https://github.com/Significant-Gravitas/AutoGPT/issues/9168
@@ -74,9 +89,9 @@ export default function AgentRunDetailsView({
Object.entries(run.inputs).map(([k, v]) => [
k,
{
title: graph.input_schema.properties[k].title,
title: graph.input_schema.properties[k]?.title,
// type: graph.input_schema.properties[k].type, // TODO: implement typed graph inputs
value: v,
value: typeof v == "object" ? JSON.stringify(v, undefined, 2) : v,
},
]),
);
@@ -106,7 +121,11 @@ export default function AgentRunDetailsView({
const agentRunOutputs:
| Record<
string,
{ title?: string; /* type: BlockIOSubType; */ values: Array<any> }
{
title?: string;
/* type: BlockIOSubType; */
values: Array<React.ReactNode>;
}
>
| null
| undefined = useMemo(() => {
@@ -115,12 +134,14 @@ export default function AgentRunDetailsView({
// Add type info from agent input schema
return Object.fromEntries(
Object.entries(run.outputs).map(([k, v]) => [
Object.entries(run.outputs).map(([k, vv]) => [
k,
{
title: graph.output_schema.properties[k].title,
/* type: agent.output_schema.properties[k].type */
values: v,
values: vv.map((v) =>
typeof v == "object" ? JSON.stringify(v, undefined, 2) : v,
),
},
]),
);
@@ -142,7 +163,8 @@ export default function AgentRunDetailsView({
},
] satisfies ButtonAction[])
: []),
...(["success", "failed", "stopped"].includes(runStatus)
...(["success", "failed", "stopped"].includes(runStatus) &&
!graph.has_webhook_trigger
? [
{
label: (
@@ -155,9 +177,27 @@ export default function AgentRunDetailsView({
},
]
: []),
...(agent.can_access_graph
? [
{
label: "Open run in builder",
href: `/build?flowID=${run.graph_id}&flowVersion=${run.graph_version}&flowExecutionID=${run.id}`,
},
]
: []),
{ label: "Delete run", variant: "secondary", callback: deleteRun },
],
[runStatus, runAgain, stopRun, deleteRun],
[
runStatus,
runAgain,
stopRun,
deleteRun,
graph.has_webhook_trigger,
agent.can_access_graph,
run.graph_id,
run.graph_version,
run.id,
],
);
return (
@@ -235,31 +275,9 @@ export default function AgentRunDetailsView({
{/* Run / Agent Actions */}
<aside className="w-48 xl:w-56">
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Run actions</h3>
{runActions.map((action, i) => (
<Button
key={i}
variant={action.variant ?? "outline"}
onClick={action.callback}
>
{action.label}
</Button>
))}
</div>
<ActionButtonGroup title="Run actions" actions={runActions} />
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Agent actions</h3>
{agentActions.map((action, i) => (
<Button
key={i}
variant={action.variant ?? "outline"}
onClick={action.callback}
>
{action.label}
</Button>
))}
</div>
<ActionButtonGroup title="Agent actions" actions={agentActions} />
</div>
</aside>
</div>

View File

@@ -8,9 +8,9 @@ import type { ButtonAction } from "@/components/agptui/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { TypeBasedInput } from "@/components/type-based-input";
import { useToastOnFail } from "@/components/ui/use-toast";
import ActionButtonGroup from "@/components/agptui/action-button-group";
import SchemaTooltip from "@/components/SchemaTooltip";
import { IconPlay } from "@/components/ui/icons";
import { Button } from "@/components/agptui/Button";
export default function AgentRunDraftView({
graph,
@@ -87,31 +87,9 @@ export default function AgentRunDraftView({
{/* Actions */}
<aside className="w-48 xl:w-56">
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Run actions</h3>
{runActions.map((action, i) => (
<Button
key={i}
variant={action.variant ?? "outline"}
onClick={action.callback}
>
{action.label}
</Button>
))}
</div>
<ActionButtonGroup title="Run actions" actions={runActions} />
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Agent actions</h3>
{agentActions.map((action, i) => (
<Button
key={i}
variant={action.variant ?? "outline"}
onClick={action.callback}
>
{action.label}
</Button>
))}
</div>
<ActionButtonGroup title="Agent actions" actions={agentActions} />
</div>
</aside>
</div>

View File

@@ -23,6 +23,7 @@ interface AgentRunsSelectorListProps {
agentRuns: GraphExecutionMeta[];
schedules: Schedule[];
selectedView: { type: "run" | "schedule"; id?: string };
allowDraftNewRun?: boolean;
onSelectRun: (id: GraphExecutionID) => void;
onSelectSchedule: (schedule: Schedule) => void;
onSelectDraftNewRun: () => void;
@@ -36,6 +37,7 @@ export default function AgentRunsSelectorList({
agentRuns,
schedules,
selectedView,
allowDraftNewRun = true,
onSelectRun,
onSelectSchedule,
onSelectDraftNewRun,
@@ -49,19 +51,21 @@ export default function AgentRunsSelectorList({
return (
<aside className={cn("flex flex-col gap-4", className)}>
<Button
size="card"
className={
"mb-4 hidden h-16 w-72 items-center gap-2 py-6 lg:flex xl:w-80 " +
(selectedView.type == "run" && !selectedView.id
? "agpt-card-selected text-accent"
: "")
}
onClick={onSelectDraftNewRun}
>
<Plus className="h-6 w-6" />
<span>New run</span>
</Button>
{allowDraftNewRun && (
<Button
size="card"
className={
"mb-4 hidden h-16 w-72 items-center gap-2 py-6 lg:flex xl:w-80 " +
(selectedView.type == "run" && !selectedView.id
? "agpt-card-selected text-accent"
: "")
}
onClick={onSelectDraftNewRun}
>
<Plus className="h-6 w-6" />
<span>New run</span>
</Button>
)}
<div className="flex gap-2">
<Badge
@@ -89,19 +93,21 @@ export default function AgentRunsSelectorList({
<ScrollArea className="lg:h-[calc(100vh-200px)]">
<div className="flex gap-2 lg:flex-col">
{/* New Run button - only in small layouts */}
<Button
size="card"
className={
"flex h-28 w-40 items-center gap-2 py-6 lg:hidden " +
(selectedView.type == "run" && !selectedView.id
? "agpt-card-selected text-accent"
: "")
}
onClick={onSelectDraftNewRun}
>
<Plus className="h-6 w-6" />
<span>New run</span>
</Button>
{allowDraftNewRun && (
<Button
size="card"
className={
"flex h-28 w-40 items-center gap-2 py-6 lg:hidden " +
(selectedView.type == "run" && !selectedView.id
? "agpt-card-selected text-accent"
: "")
}
onClick={onSelectDraftNewRun}
>
<Plus className="h-6 w-6" />
<span>New run</span>
</Button>
)}
{activeListTab === "runs"
? agentRuns

View File

@@ -12,7 +12,7 @@ import type { ButtonAction } from "@/components/agptui/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AgentRunStatus } from "@/components/agents/agent-run-status-chip";
import { useToastOnFail } from "@/components/ui/use-toast";
import { Button } from "@/components/agptui/Button";
import ActionButtonGroup from "@/components/agptui/action-button-group";
import { Input } from "@/components/ui/input";
export default function AgentScheduleDetailsView({
@@ -75,7 +75,7 @@ export default function AgentScheduleDetailsView({
[api, graph, schedule, onForcedRun, toastOnFail],
);
const runActions: { label: string; callback: () => void }[] = useMemo(
const runActions: ButtonAction[] = useMemo(
() => [{ label: "Run now", callback: () => runNow() }],
[runNow],
);
@@ -126,27 +126,9 @@ export default function AgentScheduleDetailsView({
{/* Run / Agent Actions */}
<aside className="w-48 xl:w-56">
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Run actions</h3>
{runActions.map((action, i) => (
<Button key={i} variant="outline" onClick={action.callback}>
{action.label}
</Button>
))}
</div>
<ActionButtonGroup title="Run actions" actions={runActions} />
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Agent actions</h3>
{agentActions.map((action, i) => (
<Button
key={i}
variant={action.variant ?? "outline"}
onClick={action.callback}
>
{action.label}
</Button>
))}
</div>
<ActionButtonGroup title="Agent actions" actions={agentActions} />
</div>
</aside>
</div>

View File

@@ -21,16 +21,13 @@ export const BecomeACreator: React.FC<BecomeACreatorProps> = ({
return (
<div className="relative mx-auto h-auto min-h-[300px] w-full max-w-[1360px] md:min-h-[400px] lg:h-[459px]">
{/* Top border */}
<div className="left-0 top-0 h-px w-full bg-gray-200 dark:bg-gray-700" />
{/* Title */}
<h2 className="underline-from-font decoration-skip-ink-none mt-[25px] text-left font-poppins text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
<h2 className="mb-[77px] text-left font-poppins text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
{title}
</h2>
{/* Content Container */}
<div className="m-auto w-full max-w-[900px] px-4 py-16 text-center md:px-6 lg:px-0">
<div className="m-auto w-full max-w-[900px] px-4 pb-16 text-center md:px-6 lg:px-0">
<h2 className="underline-from-font decoration-skip-ink-none mb-6 text-center font-poppins text-[48px] font-semibold leading-[54px] tracking-[-0.012em] text-neutral-950 dark:text-neutral-50 md:mb-8 lg:mb-12">
Build AI agents and share
<br />

View File

@@ -27,8 +27,20 @@ export const CreatorInfoCard: React.FC<CreatorInfoCardProps> = ({
>
<div className="flex w-full flex-col items-start justify-start gap-3.5 sm:h-[218px]">
<Avatar className="h-[100px] w-[100px] sm:h-[130px] sm:w-[130px]">
<AvatarImage src={avatarSrc} alt={`${username}'s avatar`} />
<AvatarFallback className="h-[100px] w-[100px] sm:h-[130px] sm:w-[130px]">
<AvatarFallback
size={130}
className="h-[100px] w-[100px] sm:h-[130px] sm:w-[130px]"
>
<AvatarImage
width={130}
height={130}
src={avatarSrc}
alt={`${username}'s avatar`}
/>
<AvatarFallback
size={130}
>
className="h-[100px] w-[100px] sm:h-[130px] sm:w-[130px]"
{username.charAt(0)}
</AvatarFallback>
</Avatar>
@@ -77,7 +89,7 @@ export const CreatorInfoCard: React.FC<CreatorInfoCardProps> = ({
Average rating
</div>
<div className="inline-flex items-center gap-2">
<div className="font-geist text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
<div className="font-sans text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
{averageRating.toFixed(1)}
</div>
<div
@@ -93,7 +105,7 @@ export const CreatorInfoCard: React.FC<CreatorInfoCardProps> = ({
<div className="w-full font-neue text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
Number of runs
</div>
<div className="font-geist text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
<div className="font-sans text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
{new Intl.NumberFormat().format(totalRuns)} runs
</div>
</div>

View File

@@ -44,7 +44,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({
<form
onSubmit={handleSubmit}
data-testid="store-search-bar"
className={`${width} ${height} px-4 py-2 md:px-6 md:py-1 ${backgroundColor} flex items-center justify-center gap-2 rounded-full md:gap-5`}
className={`${width} ${height} px-4 pt-2 md:px-6 md:pt-1 ${backgroundColor} flex items-center justify-center gap-2 rounded-full md:gap-5`}
>
<MagnifyingGlassIcon className={`h-5 w-5 md:h-7 md:w-7 ${iconColor}`} />
<input

View File

@@ -75,22 +75,22 @@ export const StoreCard: React.FC<StoreCardProps> = ({
{/* Content Section */}
<div className="w-full px-2 py-4">
{/* Title and Creator */}
<h3 className="mb-0.5 font-poppins text-2xl font-semibold leading-tight text-[#272727] dark:text-neutral-100">
<h3 className="mb-0.5 font-poppins text-2xl font-semibold text-[#272727] dark:text-neutral-100">
{agentName}
</h3>
{!hideAvatar && creatorName && (
<p className="font-lead mb-2.5 text-base font-normal text-neutral-600 dark:text-neutral-400">
<p className="mb-2.5 font-sans text-xl font-normal text-neutral-600 dark:text-neutral-400">
by {creatorName}
</p>
)}
{/* Description */}
<p className="font-geist mb-4 line-clamp-3 text-base font-normal leading-normal text-neutral-600 dark:text-neutral-400">
<p className="mb-4 font-sans text-base font-normal leading-normal text-neutral-600 dark:text-neutral-400">
{description}
</p>
{/* Stats Row */}
<div className="flex items-center justify-between">
<div className="font-geist text-lg font-semibold text-neutral-800 dark:text-neutral-200">
<div className="font-sans text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{runs.toLocaleString()} runs
</div>
<div className="flex items-center gap-2">

View File

@@ -0,0 +1,41 @@
import React from "react";
import { cn } from "@/lib/utils";
import type { ButtonAction } from "@/components/agptui/types";
import { Button, buttonVariants } from "@/components/agptui/Button";
import Link from "next/link";
export default function ActionButtonGroup({
title,
actions,
className,
}: {
title: React.ReactNode;
actions: ButtonAction[];
className?: string;
}): React.ReactElement {
return (
<div className={cn("flex flex-col gap-3", className)}>
<h3 className="text-sm font-medium">{title}</h3>
{actions.map((action, i) =>
"callback" in action ? (
<Button
key={i}
variant={action.variant ?? "outline"}
onClick={action.callback}
>
{action.label}
</Button>
) : (
<Link
key={i}
className={buttonVariants({ variant: action.variant })}
href={action.href}
>
{action.label}
</Link>
),
)}
</div>
);
}

View File

@@ -45,9 +45,9 @@ export const AgentsSection: React.FC<AgentsSectionProps> = ({
};
return (
<div className="flex flex-col items-center justify-center py-4 lg:py-8">
<div className="flex flex-col items-center justify-center">
<div className="w-full max-w-[1360px]">
<div className="decoration-skip-ink-none mb-8 text-left font-poppins text-[18px] font-[600] leading-7 text-[#282828] underline-offset-[from-font] dark:text-neutral-200">
<div className="mb-8 text-left font-poppins text-[18px] font-[600] leading-7 text-[#282828] dark:text-neutral-200">
{sectionTitle}
</div>
{!displayedAgents || displayedAgents.length === 0 ? (

View File

@@ -31,7 +31,7 @@ export const FeaturedCreators: React.FC<FeaturedCreatorsProps> = ({
const displayedCreators = featuredCreators.slice(0, 4);
return (
<div className="flex w-full flex-col items-center justify-center py-16">
<div className="flex w-full flex-col items-center justify-center">
<div className="w-full max-w-[1360px]">
<h2 className="mb-8 font-poppins text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
{title}

View File

@@ -46,7 +46,7 @@ export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
};
return (
<section className="mx-auto w-full max-w-7xl px-4 pb-16">
<section className="mx-auto w-full max-w-7xl px-4">
<h2 className="mb-8 font-poppins text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
Featured agents
</h2>

View File

@@ -36,7 +36,7 @@ export const HeroSection: React.FC = () => {
<h3 className="mb:text-2xl mb-6 text-center font-sans text-xl font-normal leading-loose text-neutral-700 dark:text-neutral-300 md:mb-12">
Bringing you AI agents designed by thinkers from around the world
</h3>
<div className="mb-4 flex justify-center sm:mb-5 md:mb-6">
<div className="mb-4 flex justify-center sm:mb-5">
<SearchBar height="h-[74px]" />
</div>
<div>

View File

@@ -4,5 +4,4 @@ import React from "react";
export type ButtonAction = {
label: React.ReactNode;
variant?: ButtonProps["variant"];
callback: () => void;
};
} & ({ callback: () => void } | { href: string });

View File

@@ -7,14 +7,12 @@ import { BehaveAs } from "@/lib/utils";
interface Props {
message?: string | null;
isError?: boolean;
showErrorPrompt?: boolean;
behaveAs?: BehaveAs;
}
export default function AuthFeedback({
message = "",
isError = false,
showErrorPrompt = false,
behaveAs = BehaveAs.CLOUD,
}: Props) {
// If there's no message but isError is true, show a default error message
@@ -41,7 +39,7 @@ export default function AuthFeedback({
)}
{/* Cloud-specific help */}
{showErrorPrompt && behaveAs === BehaveAs.CLOUD && (
{isError && behaveAs === BehaveAs.CLOUD && (
<div className="mt-2 space-y-2 text-sm">
<span className="block text-center font-medium text-red-500">
The provided email may not be allowed to sign up.
@@ -85,7 +83,7 @@ export default function AuthFeedback({
)}
{/* Local-specific help */}
{showErrorPrompt && behaveAs === BehaveAs.LOCAL && (
{isError && behaveAs === BehaveAs.LOCAL && (
<Card className="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm">
<CardContent className="p-0">
<div className="space-y-4 divide-y divide-slate-100">

View File

@@ -1,7 +1,6 @@
"use client";
import { useState } from "react";
import { Upload, X } from "lucide-react";
import { removeCredentials } from "@/lib/utils";
import { Button } from "@/components/agptui/Button";
import {
Dialog,
@@ -24,8 +23,11 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Graph, GraphCreatable } from "@/lib/autogpt-server-api";
import { updatedBlockIDMap } from "@/components/agent-import-form";
import {
Graph,
GraphCreatable,
sanitizeImportedGraph,
} from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { useToast } from "@/components/ui/use-toast";
@@ -41,15 +43,6 @@ const formSchema = z.object({
agentDescription: z.string(),
});
function updateBlockIDs(graph: Graph) {
graph.nodes
.filter((node) => node.block_id in updatedBlockIDMap)
.forEach((node) => {
node.block_id = updatedBlockIDMap[node.block_id];
});
return graph;
}
export default function LibraryUploadAgentDialog(): React.ReactNode {
const [isDroped, setisDroped] = useState(false);
const [isLoading, setIsLoading] = useState(false);
@@ -120,8 +113,7 @@ export default function LibraryUploadAgentDialog(): React.ReactNode {
);
}
const agent = obj as Graph;
removeCredentials(agent);
updateBlockIDs(agent);
sanitizeImportedGraph(agent);
setAgentObject(agent);
if (!form.getValues("agentName")) {
form.setValue("agentName", agent.name);

View File

@@ -2,7 +2,6 @@ import React, { useEffect, useState, useCallback } from "react";
import {
GraphExecutionMeta,
Graph,
safeCopyGraph,
BlockUIType,
BlockIORootSchema,
LibraryAgent,
@@ -208,16 +207,15 @@ export const FlowInfo: React.FC<
className="px-2.5"
title="Export to a JSON-file"
data-testid="export-button"
onClick={async () =>
exportAsJSONFile(
safeCopyGraph(
flowVersions!.find(
(v) => v.version == selectedFlowVersion!.version,
)!,
await api.getBlocks(),
),
`${flow.name}_v${selectedFlowVersion!.version}.json`,
)
onClick={() =>
api
.getGraph(flow.agent_id, selectedFlowVersion!.version, true)
.then((graph) =>
exportAsJSONFile(
graph,
`${flow.name}_v${selectedFlowVersion!.version}.json`,
),
)
}
>
<ExitIcon className="mr-2" /> Export

View File

@@ -115,11 +115,13 @@ export const FlowRunInfo: React.FC<
<strong>Finished:</strong>{" "}
{moment(execution.ended_at).format("YYYY-MM-DD HH:mm:ss")}
</p>
<p>
<strong>Duration (run time):</strong>{" "}
{execution.duration.toFixed(1)} (
{execution.total_run_time.toFixed(1)}) seconds
</p>
{execution.stats && (
<p>
<strong>Duration (run time):</strong>{" "}
{execution.stats.duration.toFixed(1)} (
{execution.stats.node_exec_time.toFixed(1)}) seconds
</p>
)}
</CardContent>
</Card>
<RunnerOutputUI

View File

@@ -62,7 +62,11 @@ export const FlowRunsList: React.FC<{
className="w-full justify-center"
/>
</TableCell>
<TableCell>{formatDuration(execution.duration)}</TableCell>
<TableCell>
{execution.stats
? formatDuration(execution.stats.duration)
: ""}
</TableCell>
</TableRow>
))}
</TableBody>

View File

@@ -105,16 +105,16 @@ export const FlowRunsStatus: React.FC<{
<p>
<strong>Total run time:</strong>{" "}
{filteredFlowRuns.reduce(
(total, run) => total + run.total_run_time,
(total, run) => total + (run.stats?.node_exec_time ?? 0),
0,
)}{" "}
seconds
</p>
{filteredFlowRuns.some((r) => r.cost) && (
{filteredFlowRuns.some((r) => r.stats) && (
<p>
<strong>Total cost:</strong>{" "}
{filteredFlowRuns.reduce(
(total, run) => total + (run.cost ?? 0),
(total, run) => total + (run.stats?.cost ?? 0),
0,
)}{" "}
seconds

View File

@@ -81,11 +81,13 @@ export const FlowRunsTimeline = ({
<strong>Started:</strong>{" "}
{moment(data.started_at).format("YYYY-MM-DD HH:mm:ss")}
</p>
<p>
<strong>Duration / run time:</strong>{" "}
{formatDuration(data.duration)} /{" "}
{formatDuration(data.total_run_time)}
</p>
{data.stats && (
<p>
<strong>Duration / run time:</strong>{" "}
{formatDuration(data.stats.duration)} /{" "}
{formatDuration(data.stats.node_exec_time)}
</p>
)}
</Card>
);
}
@@ -99,8 +101,9 @@ export const FlowRunsTimeline = ({
.filter((e) => e.graph_id == flow.agent_id)
.map((e) => ({
...e,
time: e.started_at.getTime() + e.total_run_time * 1000,
_duration: e.total_run_time,
time:
e.started_at.getTime() + (e.stats?.node_exec_time ?? 0) * 1000,
_duration: e.stats?.node_exec_time ?? 0,
}))}
name={flow.name}
fill={`hsl(${(hashString(flow.id) * 137.5) % 360}, 70%, 50%)`}
@@ -120,7 +123,7 @@ export const FlowRunsTimeline = ({
{
...execution,
time: execution.ended_at.getTime(),
_duration: execution.total_run_time,
_duration: execution.stats?.node_exec_time ?? 0,
},
]}
stroke={`hsl(${(hashString(execution.graph_id) * 137.5) % 360}, 70%, 50%)`}

View File

@@ -73,7 +73,7 @@ export const TypeBasedInput: FC<
case DataType.LONG_TEXT:
innerInputElement = (
<Textarea
className="rounded-[12px] px-3 py-2"
className="rounded-xl px-3 py-2"
value={value ?? ""}
placeholder={placeholder || "Enter text"}
onChange={(e) => onChange(e.target.value)}
@@ -85,9 +85,11 @@ export const TypeBasedInput: FC<
case DataType.BOOLEAN:
innerInputElement = (
<>
<span className="text-sm text-gray-500">{placeholder}</span>
<span className="text-sm text-gray-500">
{placeholder || (value ? "Enabled" : "Disabled")}
</span>
<Switch
className={placeholder ? "ml-auto" : "mx-auto"}
className="ml-auto"
checked={!!value}
onCheckedChange={(checked) => onChange(checked)}
{...props}
@@ -145,11 +147,14 @@ export const TypeBasedInput: FC<
innerInputElement = (
<Select value={value ?? ""} onValueChange={(val) => onChange(val)}>
<SelectTrigger
className={cn(inputClasses, "text-sm text-gray-500")}
className={cn(
inputClasses,
"agpt-border-input text-sm text-gray-500",
)}
>
<SelectValue placeholder={placeholder || "Select an option"} />
</SelectTrigger>
<SelectContent className="rounded-[12px] border">
<SelectContent className="rounded-xl">
{schema.enum
.filter((opt) => opt)
.map((opt) => (
@@ -198,7 +203,7 @@ export function DatePicker({
<Button
variant="outline"
className={cn(
"w-full justify-start font-normal",
"agpt-border-input w-full justify-start font-normal",
!value && "text-muted-foreground",
className,
)}
@@ -250,7 +255,9 @@ export function TimePicker({ value, onChange }: TimePickerProps) {
value={hour}
onValueChange={(val) => changeTime(val, minute, meridiem)}
>
<SelectTrigger className={cn("text-center", inputClasses)}>
<SelectTrigger
className={cn("agpt-border-input ml-1 text-center", inputClasses)}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -272,7 +279,9 @@ export function TimePicker({ value, onChange }: TimePickerProps) {
value={minute}
onValueChange={(val) => changeTime(hour, val, meridiem)}
>
<SelectTrigger className={cn("text-center", inputClasses)}>
<SelectTrigger
className={cn("agpt-border-input text-center", inputClasses)}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -290,7 +299,9 @@ export function TimePicker({ value, onChange }: TimePickerProps) {
value={meridiem}
onValueChange={(val) => changeTime(hour, minute, val)}
>
<SelectTrigger className={cn("text-center", inputClasses)}>
<SelectTrigger
className={cn("agpt-border-input text-center", inputClasses)}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -378,7 +389,7 @@ const FileInput: FC<FileInputProps> = ({
<div className={cn("w-full", className)}>
{value ? (
<div className="flex min-h-14 items-center gap-4">
<div className="agpt-border-input flex min-h-14 w-full items-center justify-between rounded-[12px] bg-zinc-50 p-4 text-sm text-gray-500">
<div className="agpt-border-input flex min-h-14 w-full items-center justify-between rounded-xl bg-zinc-50 p-4 text-sm text-gray-500">
<div className="flex items-center gap-2">
<FileTextIcon className="h-7 w-7 text-black" />
<div className="flex flex-col gap-0.5">
@@ -402,7 +413,7 @@ const FileInput: FC<FileInputProps> = ({
<div
onDrop={handleFileDrop}
onDragOver={(e) => e.preventDefault()}
className="agpt-border-input flex min-h-14 w-full items-center justify-center rounded-[12px] border-dashed bg-zinc-50 text-sm text-gray-500"
className="agpt-border-input flex min-h-14 w-full items-center justify-center rounded-xl border-dashed bg-zinc-50 text-sm text-gray-500"
>
Choose a file or drag and drop it here
</div>

View File

@@ -58,8 +58,10 @@ const getAvatarSize = (className: string | undefined): number => {
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> & {
size?: number;
}
>(({ className, size, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
@@ -69,7 +71,7 @@ const AvatarFallback = React.forwardRef<
{...props}
>
<BoringAvatar
size={getAvatarSize(className)}
size={size || getAvatarSize(className)}
name={props.children?.toString() || "User"}
variant="marble"
colors={["#92A1C6", "#146A7C", "#F0AB3D", "#C271B4", "#C20D90"]}

View File

@@ -213,7 +213,7 @@ const CarouselPrevious = React.forwardRef<
className={cn(
"absolute h-[52px] w-[52px] rounded-full",
orientation === "horizontal"
? "-bottom-20 right-24 -translate-y-1/2"
? "right-24 top-0"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
@@ -259,7 +259,7 @@ const CarouselNext = React.forwardRef<
className={cn(
"absolute h-[52px] w-[52px] rounded-full",
orientation === "horizontal"
? "-bottom-20 right-4 -translate-y-1/2"
? "right-4 top-0"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
@@ -302,7 +302,7 @@ const CarouselIndicator = React.forwardRef<
return (
<div
ref={ref}
className={cn("relative top-10 flex h-3 items-center gap-2", className)}
className={cn("relative top-7 flex h-3 items-center gap-2", className)}
{...props}
>
{scrollSnaps.map((_, index) => (

View File

@@ -205,7 +205,7 @@ export default class BackendAPI {
return this._get(`/graphs`);
}
getGraph(
async getGraph(
id: GraphID,
version?: number,
for_export?: boolean,
@@ -217,7 +217,9 @@ export default class BackendAPI {
if (for_export !== undefined) {
query["for_export"] = for_export;
}
return this._get(`/graphs/${id}`, query);
const graph = await this._get(`/graphs/${id}`, query);
if (for_export) delete graph.user_id;
return graph;
}
getGraphAllVersions(id: GraphID): Promise<Graph[]> {

View File

@@ -249,15 +249,19 @@ export type LinkCreatable = Omit<Link, "id" | "is_static"> & {
/* Mirror of backend/data/execution.py:GraphExecutionMeta */
export type GraphExecutionMeta = {
id: GraphExecutionID;
started_at: Date;
ended_at: Date;
cost?: number;
duration: number;
total_run_time: number;
status: "QUEUED" | "RUNNING" | "COMPLETED" | "TERMINATED" | "FAILED";
user_id: UserID;
graph_id: GraphID;
graph_version: number;
preset_id?: string;
status: "QUEUED" | "RUNNING" | "COMPLETED" | "TERMINATED" | "FAILED";
started_at: Date;
ended_at: Date;
stats?: {
cost: number;
duration: number;
node_exec_time: number;
node_exec_count: number;
};
};
export type GraphExecutionID = Brand<string, "GraphExecutionID">;
@@ -271,6 +275,7 @@ export type GraphExecution = GraphExecutionMeta & {
export type GraphMeta = {
id: GraphID;
user_id: UserID;
version: number;
is_active: boolean;
name: string;
@@ -301,11 +306,18 @@ export type GraphIOSubSchema = Omit<
export type Graph = GraphMeta & {
nodes: Array<Node>;
links: Array<Link>;
has_webhook_trigger: boolean;
};
export type GraphUpdateable = Omit<
Graph,
"version" | "is_active" | "links" | "input_schema" | "output_schema"
| "user_id"
| "version"
| "is_active"
| "links"
| "input_schema"
| "output_schema"
| "has_webhook_trigger"
> & {
version?: number;
is_active?: boolean;
@@ -499,7 +511,7 @@ export type NotificationPreferenceDTO = {
};
export type NotificationPreference = NotificationPreferenceDTO & {
user_id: string;
user_id: UserID;
emails_sent_today: number;
last_reset_date: Date;
};
@@ -519,10 +531,12 @@ export type Webhook = {
};
export type User = {
id: string;
id: UserID;
email: string;
};
export type UserID = Brand<string, "UserID">;
export enum BlockUIType {
STANDARD = "Standard",
INPUT = "Input",
@@ -676,7 +690,7 @@ export type Schedule = {
id: ScheduleID;
name: string;
cron: string;
user_id: string;
user_id: UserID;
graph_id: GraphID;
graph_version: number;
input_data: { [key: string]: any };
@@ -770,7 +784,7 @@ export interface TransactionHistory {
export interface RefundRequest {
id: string;
user_id: string;
user_id: UserID;
transaction_key: string;
amount: number;
reason: string;

View File

@@ -1,25 +1,5 @@
import { Connection } from "@xyflow/react";
import { Graph, Block, Node, BlockUIType, Link } from "./types";
/** Creates a copy of the graph with all secrets removed */
export function safeCopyGraph(graph: Graph, block_defs: Block[]): Graph {
graph = removeAgentInputBlockValues(graph, block_defs);
return {
...graph,
nodes: graph.nodes.map((node) => {
const block = block_defs.find((b) => b.id == node.block_id)!;
return {
...node,
input_default: Object.keys(node.input_default)
.filter((k) => !block.inputSchema.properties[k].secret)
.reduce((obj: Node["input_default"], key) => {
obj[key] = node.input_default[key];
return obj;
}, {}),
};
}),
};
}
import { Graph, Block, BlockUIType, Link } from "./types";
export function removeAgentInputBlockValues(graph: Graph, blocks: Block[]) {
const inputBlocks = graph.nodes.filter(
@@ -53,3 +33,66 @@ export function formatEdgeID(conn: Link | Connection): string {
return `${conn.source}_${conn.sourceHandle}_${conn.target}_${conn.targetHandle}`;
}
}
/** Sanitizes a graph object in place so it can "safely" be imported into the system.
*
* **⚠️ Note:** not an actual safety feature, just intended to make the import UX more reliable.
*/
export function sanitizeImportedGraph(graph: Graph): void {
updateBlockIDs(graph);
removeCredentials(graph);
}
/** Recursively remove (in place) all "credentials" properties from an object */
function removeCredentials(obj: any): void {
if (obj && typeof obj === "object") {
if (Array.isArray(obj)) {
obj.forEach((item) => removeCredentials(item));
} else {
delete obj.credentials;
Object.values(obj).forEach((value) => removeCredentials(value));
}
}
return obj;
}
/** ⚠️ Remove after 2025-10-01 (one year after implementation in
* [#8229](https://github.com/Significant-Gravitas/AutoGPT/pull/8229))
*/
function updateBlockIDs(graph: Graph) {
graph.nodes
.filter((node) => node.block_id in updatedBlockIDMap)
.forEach((node) => {
node.block_id = updatedBlockIDMap[node.block_id];
});
}
const updatedBlockIDMap: Record<string, string> = {
// https://github.com/Significant-Gravitas/AutoGPT/issues/8223
"a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6":
"436c3984-57fd-4b85-8e9a-459b356883bd",
"b2g2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6":
"0e50422c-6dee-4145-83d6-3a5a392f65de",
"c3d4e5f6-7g8h-9i0j-1k2l-m3n4o5p6q7r8":
"a0a69be1-4528-491c-a85a-a4ab6873e3f0",
"c3d4e5f6-g7h8-i9j0-k1l2-m3n4o5p6q7r8":
"32a87eab-381e-4dd4-bdb8-4c47151be35a",
"b2c3d4e5-6f7g-8h9i-0j1k-l2m3n4o5p6q7":
"87840993-2053-44b7-8da4-187ad4ee518c",
"h1i2j3k4-5l6m-7n8o-9p0q-r1s2t3u4v5w6":
"d0822ab5-9f8a-44a3-8971-531dd0178b6b",
"d3f4g5h6-1i2j-3k4l-5m6n-7o8p9q0r1s2t":
"df06086a-d5ac-4abb-9996-2ad0acb2eff7",
"h5e7f8g9-1b2c-3d4e-5f6g-7h8i9j0k1l2m":
"f5b0f5d0-1862-4d61-94be-3ad0fa772760",
"a1234567-89ab-cdef-0123-456789abcdef":
"4335878a-394e-4e67-adf2-919877ff49ae",
"f8e7d6c5-b4a3-2c1d-0e9f-8g7h6i5j4k3l":
"f66a3543-28d3-4ab5-8945-9b336371e2ce",
"b29c1b50-5d0e-4d9f-8f9d-1b0e6fcbf0h2":
"716a67b3-6760-42e7-86dc-18645c6e00fc",
"31d1064e-7446-4693-o7d4-65e5ca9110d1":
"cc10ff7b-7753-4ff2-9af6-9399b1a7eddc",
"c6731acb-4105-4zp1-bc9b-03d0036h370g":
"5ebe6768-8e5d-41e3-9134-1c7bd89a8d52",
};

View File

@@ -1,6 +1,7 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { Category } from "./autogpt-server-api/types";
import { Category, Graph } from "@/lib/autogpt-server-api/types";
import { NodeDimension } from "@/components/Flow";
export function cn(...inputs: ClassValue[]) {
@@ -120,28 +121,9 @@ const applyExceptions = (str: string): string => {
return str;
};
/** Recursively remove all "credentials" properties from exported JSON files */
export function removeCredentials(obj: any) {
if (obj && typeof obj === "object") {
if (Array.isArray(obj)) {
obj.forEach((item) => removeCredentials(item));
} else {
delete obj.credentials;
Object.values(obj).forEach((value) => removeCredentials(value));
}
}
return obj;
}
export function exportAsJSONFile(obj: object, filename: string): void {
// Deep clone the object to avoid modifying the original
const sanitizedObj = JSON.parse(JSON.stringify(obj));
// Sanitize the object
removeCredentials(sanitizedObj);
// Create downloadable blob
const jsonString = JSON.stringify(sanitizedObj, null, 2);
const jsonString = JSON.stringify(obj, null, 2);
const blob = new Blob([jsonString], { type: "application/json" });
const url = URL.createObjectURL(blob);

View File

@@ -1,9 +1,5 @@
# Getting Started with AutoGPT: Self-Hosting Guide
This tutorial will walk you through the process of setting up AutoGPT locally on your machine.
<center><iframe width="560" height="315" src="https://www.youtube.com/embed/4Bycr6_YAMI?si=dXGhFeWrCK2UkKgj" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></center>
## Introduction
This guide will help you setup the server and builder for the project.
@@ -270,6 +266,30 @@ If you run into issues with dangling orphans, try:
docker compose down --volumes --remove-orphans && docker-compose up --force-recreate --renew-anon-volumes --remove-orphans
```
### 📌 Windows Installation Note
When installing Docker on Windows, it is **highly recommended** to select **WSL 2** instead of Hyper-V. Using Hyper-V can cause compatibility issues with Supabase, leading to the `supabase-db` container being marked as **unhealthy**.
#### **Steps to enable WSL 2 for Docker:**
1. Install [WSL 2](https://learn.microsoft.com/en-us/windows/wsl/install).
2. Ensure that your Docker settings use WSL 2 as the default backend:
- Open **Docker Desktop**.
- Navigate to **Settings > General**.
- Check **Use the WSL 2 based engine**.
3. Restart **Docker Desktop**.
#### **Already Installed Docker with Hyper-V?**
If you initially installed Docker with Hyper-V, you **dont need to reinstall** it. You can switch to WSL 2 by following these steps:
1. Open **Docker Desktop**.
2. Go to **Settings > General**.
3. Enable **Use the WSL 2 based engine**.
4. Restart Docker.
🚨 **Warning:** Enabling WSL 2 may **erase your existing containers and build history**. If you have important containers, consider backing them up before switching.
For more details, refer to [Docker's official documentation](https://docs.docker.com/desktop/windows/wsl/).
## Development
### Formatting & Linting