mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-21 04:57:58 -05:00
Compare commits
21 Commits
testing-cl
...
abhi-9003/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc3fe03674 | ||
|
|
4760b2fdad | ||
|
|
8389b583ff | ||
|
|
b782aba6b4 | ||
|
|
a1747d2521 | ||
|
|
7d83c5e57a | ||
|
|
dac7f94928 | ||
|
|
703809446c | ||
|
|
7fc5a57515 | ||
|
|
a35ade8585 | ||
|
|
cf36dcabc2 | ||
|
|
d59faa7628 | ||
|
|
1df9aef199 | ||
|
|
c49aaf558a | ||
|
|
6a3e746544 | ||
|
|
906988a945 | ||
|
|
043fa551d9 | ||
|
|
b09bd5d3b8 | ||
|
|
248cd53dec | ||
|
|
d9991832cb | ||
|
|
8f1b16aee9 |
21
.github/workflows/platform-backend-ci.yml
vendored
21
.github/workflows/platform-backend-ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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:
|
||||
We’ve 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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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?"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,5 +4,4 @@ import React from "react";
|
||||
export type ButtonAction = {
|
||||
label: React.ReactNode;
|
||||
variant?: ButtonProps["variant"];
|
||||
callback: () => void;
|
||||
};
|
||||
} & ({ callback: () => void } | { href: string });
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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%)`}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 **don’t 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
|
||||
|
||||
Reference in New Issue
Block a user