Compare commits

..

No commits in common. "pwuts/open-2088-implement-new-agent-runs-page" and "master" have entirely different histories.

84 changed files with 797 additions and 3322 deletions

View File

@ -2,13 +2,13 @@ name: AutoGPT Platform - Backend CI
on: on:
push: push:
branches: [master, dev, ci-test*, dev-*] branches: [master, dev, ci-test*]
paths: paths:
- ".github/workflows/platform-backend-ci.yml" - ".github/workflows/platform-backend-ci.yml"
- "autogpt_platform/backend/**" - "autogpt_platform/backend/**"
- "autogpt_platform/autogpt_libs/**" - "autogpt_platform/autogpt_libs/**"
pull_request: pull_request:
branches: [master, dev, release-*, dev-*] branches: [master, dev, release-*]
paths: paths:
- ".github/workflows/platform-backend-ci.yml" - ".github/workflows/platform-backend-ci.yml"
- "autogpt_platform/backend/**" - "autogpt_platform/backend/**"

View File

@ -2,7 +2,7 @@ name: AutoGPT Platform - Frontend CI
on: on:
push: push:
branches: [master, dev, dev-*] branches: [master, dev]
paths: paths:
- ".github/workflows/platform-frontend-ci.yml" - ".github/workflows/platform-frontend-ci.yml"
- "autogpt_platform/frontend/**" - "autogpt_platform/frontend/**"

View File

@ -170,16 +170,6 @@ repos:
files: ^classic/benchmark/(agbenchmark|tests)/((?!reports).)*[/.] files: ^classic/benchmark/(agbenchmark|tests)/((?!reports).)*[/.]
args: [--config=classic/benchmark/.flake8] args: [--config=classic/benchmark/.flake8]
- repo: local
hooks:
- id: prettier
name: Format (Prettier) - AutoGPT Platform - Frontend
alias: format-platform-frontend
entry: bash -c 'cd autogpt_platform/frontend && npx prettier --write $(echo "$@" | sed "s|autogpt_platform/frontend/||g")' --
files: ^autogpt_platform/frontend/
types: [file]
language: system
- repo: local - repo: local
# To have watertight type checking, we check *all* the files in an affected # To have watertight type checking, we check *all* the files in an affected
# project. To trigger on poetry.lock we also reset the file `types` filter. # project. To trigger on poetry.lock we also reset the file `types` filter.
@ -231,16 +221,6 @@ repos:
language: system language: system
pass_filenames: false pass_filenames: false
# - repo: local
# hooks:
# - id: tsc
# name: Typecheck - AutoGPT Platform - Frontend
# entry: bash -c 'cd autogpt_platform/frontend && npm run type-check'
# files: ^autogpt_platform/frontend/
# types: [file]
# language: system
# pass_filenames: false
- repo: local - repo: local
hooks: hooks:
- id: pytest - id: pytest

View File

@ -13,6 +13,7 @@ from typing_extensions import ParamSpec
from .config import SETTINGS from .config import SETTINGS
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
P = ParamSpec("P") P = ParamSpec("P")
T = TypeVar("T") T = TypeVar("T")

View File

@ -136,7 +136,6 @@ async def create_graph_execution(
graph_version: int, graph_version: int,
nodes_input: list[tuple[str, BlockInput]], nodes_input: list[tuple[str, BlockInput]],
user_id: str, user_id: str,
preset_id: str | None = None,
) -> tuple[str, list[ExecutionResult]]: ) -> tuple[str, list[ExecutionResult]]:
""" """
Create a new AgentGraphExecution record. Create a new AgentGraphExecution record.
@ -164,7 +163,6 @@ async def create_graph_execution(
] ]
}, },
"userId": user_id, "userId": user_id,
"agentPresetId": preset_id,
}, },
include=GRAPH_EXECUTION_INCLUDE, include=GRAPH_EXECUTION_INCLUDE,
) )

View File

@ -6,13 +6,7 @@ from datetime import datetime, timezone
from typing import Any, Literal, Optional, Type from typing import Any, Literal, Optional, Type
import prisma import prisma
from prisma.models import ( from prisma.models import AgentGraph, AgentGraphExecution, AgentNode, AgentNodeLink
AgentGraph,
AgentGraphExecution,
AgentNode,
AgentNodeLink,
StoreListingVersion,
)
from prisma.types import AgentGraphWhereInput from prisma.types import AgentGraphWhereInput
from pydantic.fields import computed_field from pydantic.fields import computed_field
@ -22,15 +16,12 @@ from backend.util import json
from .block import BlockInput, BlockType, get_block, get_blocks from .block import BlockInput, BlockType, get_block, get_blocks
from .db import BaseDbModel, transaction from .db import BaseDbModel, transaction
from .execution import ExecutionResult, ExecutionStatus from .execution import ExecutionStatus
from .includes import AGENT_GRAPH_INCLUDE, AGENT_NODE_INCLUDE from .includes import AGENT_GRAPH_INCLUDE, AGENT_NODE_INCLUDE
from .integrations import Webhook from .integrations import Webhook
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_INPUT_BLOCK_ID = AgentInputBlock().id
_OUTPUT_BLOCK_ID = AgentOutputBlock().id
class Link(BaseDbModel): class Link(BaseDbModel):
source_id: str source_id: str
@ -109,7 +100,7 @@ class NodeModel(Node):
Webhook.model_rebuild() Webhook.model_rebuild()
class GraphExecutionMeta(BaseDbModel): class GraphExecution(BaseDbModel):
execution_id: str execution_id: str
started_at: datetime started_at: datetime
ended_at: datetime ended_at: datetime
@ -118,71 +109,33 @@ class GraphExecutionMeta(BaseDbModel):
status: ExecutionStatus status: ExecutionStatus
graph_id: str graph_id: str
graph_version: int graph_version: int
preset_id: Optional[str]
@staticmethod @staticmethod
def from_db(_graph_exec: AgentGraphExecution): def from_db(execution: AgentGraphExecution):
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
start_time = _graph_exec.startedAt or _graph_exec.createdAt start_time = execution.startedAt or execution.createdAt
end_time = _graph_exec.updatedAt or now end_time = execution.updatedAt or now
duration = (end_time - start_time).total_seconds() duration = (end_time - start_time).total_seconds()
total_run_time = duration total_run_time = duration
try: try:
stats = json.loads(_graph_exec.stats or "{}", target_type=dict[str, Any]) stats = json.loads(execution.stats or "{}", target_type=dict[str, Any])
except ValueError: except ValueError:
stats = {} stats = {}
duration = stats.get("walltime", duration) duration = stats.get("walltime", duration)
total_run_time = stats.get("nodes_walltime", total_run_time) total_run_time = stats.get("nodes_walltime", total_run_time)
return GraphExecutionMeta( return GraphExecution(
id=_graph_exec.id, id=execution.id,
execution_id=_graph_exec.id, execution_id=execution.id,
started_at=start_time, started_at=start_time,
ended_at=end_time, ended_at=end_time,
duration=duration, duration=duration,
total_run_time=total_run_time, total_run_time=total_run_time,
status=ExecutionStatus(_graph_exec.executionStatus), status=ExecutionStatus(execution.executionStatus),
graph_id=_graph_exec.agentGraphId, graph_id=execution.agentGraphId,
graph_version=_graph_exec.agentGraphVersion, graph_version=execution.agentGraphVersion,
preset_id=_graph_exec.agentPresetId,
)
class GraphExecution(GraphExecutionMeta):
inputs: dict[str, Any]
outputs: dict[str, Any]
node_executions: list[ExecutionResult]
@staticmethod
def from_db(_graph_exec: AgentGraphExecution):
if _graph_exec.AgentNodeExecutions is None:
raise ValueError("Node executions must be included in query")
graph_exec = GraphExecutionMeta.from_db(_graph_exec)
node_executions = [
ExecutionResult.from_db(ne) for ne in _graph_exec.AgentNodeExecutions
]
inputs = {
exec.input_data["name"]: exec.input_data["value"]
for exec in node_executions
if exec.block_id == _INPUT_BLOCK_ID
}
outputs = {
exec.input_data["name"]: exec.input_data["value"]
for exec in node_executions
if exec.block_id == _OUTPUT_BLOCK_ID
}
return GraphExecution(
**{
field_name: getattr(graph_exec, field_name)
for field_name in graph_exec.model_fields
},
inputs=inputs,
outputs=outputs,
node_executions=node_executions,
) )
@ -558,45 +511,17 @@ async def get_graphs(
return graph_models return graph_models
async def get_graphs_executions(user_id: str) -> list[GraphExecutionMeta]: async def get_executions(user_id: str) -> list[GraphExecution]:
executions = await AgentGraphExecution.prisma().find_many( executions = await AgentGraphExecution.prisma().find_many(
where={"userId": user_id}, where={"userId": user_id},
order={"createdAt": "desc"}, order={"createdAt": "desc"},
) )
return [GraphExecutionMeta.from_db(execution) for execution in executions] return [GraphExecution.from_db(execution) for execution in executions]
async def get_graph_executions(graph_id: str, user_id: str) -> list[GraphExecutionMeta]:
executions = await AgentGraphExecution.prisma().find_many(
where={"agentGraphId": graph_id, "userId": user_id},
order={"createdAt": "desc"},
)
return [GraphExecutionMeta.from_db(execution) for execution in executions]
async def get_execution_meta(
user_id: str, execution_id: str
) -> GraphExecutionMeta | None:
execution = await AgentGraphExecution.prisma().find_first(
where={"id": execution_id, "userId": user_id}
)
return GraphExecutionMeta.from_db(execution) if execution else None
async def get_execution(user_id: str, execution_id: str) -> GraphExecution | None: async def get_execution(user_id: str, execution_id: str) -> GraphExecution | None:
execution = await AgentGraphExecution.prisma().find_first( execution = await AgentGraphExecution.prisma().find_first(
where={"id": execution_id, "userId": user_id}, where={"id": execution_id, "userId": user_id}
include={
"AgentNodeExecutions": {
"include": {"AgentNode": True, "Input": True, "Output": True},
"order_by": [
{"queuedTime": "asc"},
{ # Fallback: Incomplete execs has no queuedTime.
"addedTime": "asc"
},
],
},
},
) )
return GraphExecution.from_db(execution) if execution else None return GraphExecution.from_db(execution) if execution else None
@ -604,6 +529,7 @@ async def get_execution(user_id: str, execution_id: str) -> GraphExecution | Non
async def get_graph( async def get_graph(
graph_id: str, graph_id: str,
version: int | None = None, version: int | None = None,
template: bool = False,
user_id: str | None = None, user_id: str | None = None,
for_export: bool = False, for_export: bool = False,
) -> GraphModel | None: ) -> GraphModel | None:
@ -617,36 +543,21 @@ async def get_graph(
where_clause: AgentGraphWhereInput = { where_clause: AgentGraphWhereInput = {
"id": graph_id, "id": graph_id,
} }
if version is not None: if version is not None:
where_clause["version"] = version where_clause["version"] = version
else: elif not template:
where_clause["isActive"] = True where_clause["isActive"] = True
# TODO: Fix hack workaround to get adding store agents to work
if user_id is not None and not template:
where_clause["userId"] = user_id
graph = await AgentGraph.prisma().find_first( graph = await AgentGraph.prisma().find_first(
where=where_clause, where=where_clause,
include=AGENT_GRAPH_INCLUDE, include=AGENT_GRAPH_INCLUDE,
order={"version": "desc"}, order={"version": "desc"},
) )
return GraphModel.from_db(graph, for_export) if graph else None
# The Graph has to be owned by the user or a store listing.
if (
graph is None
or graph.userId != user_id
and not (
await StoreListingVersion.prisma().find_first(
where=prisma.types.StoreListingVersionWhereInput(
agentId=graph_id,
agentVersion=version or graph.version,
isDeleted=False,
StoreListing={"is": {"isApproved": True}},
)
)
)
):
return None
return GraphModel.from_db(graph, for_export)
async def set_graph_active_version(graph_id: str, version: int, user_id: str) -> None: async def set_graph_active_version(graph_id: str, version: int, user_id: str) -> None:
@ -700,7 +611,9 @@ async def create_graph(graph: Graph, user_id: str) -> GraphModel:
async with transaction() as tx: async with transaction() as tx:
await __create_graph(tx, graph, user_id) await __create_graph(tx, graph, user_id)
if created_graph := await get_graph(graph.id, graph.version, user_id=user_id): if created_graph := await get_graph(
graph.id, graph.version, graph.is_template, user_id=user_id
):
return created_graph return created_graph
raise ValueError(f"Created graph {graph.id} v{graph.version} is not in DB") raise ValueError(f"Created graph {graph.id} v{graph.version} is not in DB")

View File

@ -8,7 +8,7 @@ import threading
from concurrent.futures import Future, ProcessPoolExecutor from concurrent.futures import Future, ProcessPoolExecutor
from contextlib import contextmanager from contextlib import contextmanager
from multiprocessing.pool import AsyncResult, Pool from multiprocessing.pool import AsyncResult, Pool
from typing import TYPE_CHECKING, Any, Generator, Optional, TypeVar, cast from typing import TYPE_CHECKING, Any, Generator, TypeVar, cast
from redis.lock import Lock as RedisLock from redis.lock import Lock as RedisLock
@ -780,8 +780,7 @@ class ExecutionManager(AppService):
graph_id: str, graph_id: str,
data: BlockInput, data: BlockInput,
user_id: str, user_id: str,
graph_version: Optional[int] = None, graph_version: int | None = None,
preset_id: str | None = None,
) -> GraphExecutionEntry: ) -> GraphExecutionEntry:
graph: GraphModel | None = self.db_client.get_graph( graph: GraphModel | None = self.db_client.get_graph(
graph_id=graph_id, user_id=user_id, version=graph_version graph_id=graph_id, user_id=user_id, version=graph_version
@ -830,7 +829,6 @@ class ExecutionManager(AppService):
graph_version=graph.version, graph_version=graph.version,
nodes_input=nodes_input, nodes_input=nodes_input,
user_id=user_id, user_id=user_id,
preset_id=preset_id,
) )
starting_node_execs = [] starting_node_execs = []

View File

@ -63,10 +63,7 @@ def execute_graph(**kwargs):
try: try:
log(f"Executing recurring job for graph #{args.graph_id}") log(f"Executing recurring job for graph #{args.graph_id}")
get_execution_client().add_execution( get_execution_client().add_execution(
graph_id=args.graph_id, args.graph_id, args.input_data, args.user_id
data=args.input_data,
user_id=args.user_id,
graph_version=args.graph_version,
) )
except Exception as e: except Exception as e:
logger.exception(f"Error executing graph {args.graph_id}: {e}") logger.exception(f"Error executing graph {args.graph_id}: {e}")

View File

@ -320,8 +320,7 @@ async def webhook_ingress_generic(
continue continue
logger.debug(f"Executing graph #{node.graph_id} node #{node.id}") logger.debug(f"Executing graph #{node.graph_id} node #{node.id}")
executor.add_execution( executor.add_execution(
graph_id=node.graph_id, node.graph_id,
graph_version=node.graph_version,
data={f"webhook_{webhook_id}_payload": payload}, data={f"webhook_{webhook_id}_payload": payload},
user_id=webhook.user_id, user_id=webhook.user_id,
) )

View File

@ -56,18 +56,3 @@ class SetGraphActiveVersion(pydantic.BaseModel):
class UpdatePermissionsRequest(pydantic.BaseModel): class UpdatePermissionsRequest(pydantic.BaseModel):
permissions: List[APIKeyPermission] permissions: List[APIKeyPermission]
class Pagination(pydantic.BaseModel):
total_items: int = pydantic.Field(
description="Total number of items.", examples=[42]
)
total_pages: int = pydantic.Field(
description="Total number of pages.", examples=[2]
)
current_page: int = pydantic.Field(
description="Current_page page number.", examples=[1]
)
page_size: int = pydantic.Field(
description="Number of items per page.", examples=[25]
)

View File

@ -2,7 +2,6 @@ import contextlib
import logging import logging
import typing import typing
import autogpt_libs.auth.models
import fastapi import fastapi
import fastapi.responses import fastapi.responses
import starlette.middleware.cors import starlette.middleware.cors
@ -17,10 +16,7 @@ import backend.data.db
import backend.data.graph import backend.data.graph
import backend.data.user import backend.data.user
import backend.server.routers.v1 import backend.server.routers.v1
import backend.server.v2.library.db
import backend.server.v2.library.model
import backend.server.v2.library.routes import backend.server.v2.library.routes
import backend.server.v2.store.model
import backend.server.v2.store.routes import backend.server.v2.store.routes
import backend.util.service import backend.util.service
import backend.util.settings import backend.util.settings
@ -121,27 +117,9 @@ class AgentServer(backend.util.service.AppProcess):
@staticmethod @staticmethod
async def test_execute_graph( async def test_execute_graph(
graph_id: str, graph_id: str, node_input: dict[typing.Any, typing.Any], user_id: str
graph_version: int,
node_input: dict[typing.Any, typing.Any],
user_id: str,
): ):
return backend.server.routers.v1.execute_graph( return backend.server.routers.v1.execute_graph(graph_id, node_input, user_id)
user_id=user_id,
graph_id=graph_id,
graph_version=graph_version,
node_input=node_input,
)
@staticmethod
async def test_get_graph(
graph_id: str,
graph_version: int,
user_id: str,
):
return await backend.server.routers.v1.get_graph(
graph_id, user_id, graph_version
)
@staticmethod @staticmethod
async def test_create_graph( async def test_create_graph(
@ -152,7 +130,7 @@ class AgentServer(backend.util.service.AppProcess):
@staticmethod @staticmethod
async def test_get_graph_run_status(graph_exec_id: str, user_id: str): async def test_get_graph_run_status(graph_exec_id: str, user_id: str):
execution = await backend.data.graph.get_execution_meta( execution = await backend.data.graph.get_execution(
user_id=user_id, execution_id=graph_exec_id user_id=user_id, execution_id=graph_exec_id
) )
if not execution: if not execution:
@ -160,85 +138,16 @@ class AgentServer(backend.util.service.AppProcess):
return execution.status return execution.status
@staticmethod @staticmethod
async def test_get_graph_run_results( async def test_get_graph_run_node_execution_results(
graph_id: str, graph_exec_id: str, user_id: str graph_id: str, graph_exec_id: str, user_id: str
): ):
return await backend.server.routers.v1.get_graph_execution( return await backend.server.routers.v1.get_graph_run_node_execution_results(
graph_id, graph_exec_id, user_id graph_id, graph_exec_id, user_id
) )
@staticmethod @staticmethod
async def test_delete_graph(graph_id: str, user_id: str): async def test_delete_graph(graph_id: str, user_id: str):
await backend.server.v2.library.db.delete_library_agent_by_graph_id(
graph_id=graph_id, user_id=user_id
)
return await backend.server.routers.v1.delete_graph(graph_id, user_id) return await backend.server.routers.v1.delete_graph(graph_id, user_id)
@staticmethod
async def test_get_presets(user_id: str, page: int = 1, page_size: int = 10):
return await backend.server.v2.library.routes.presets.get_presets(
user_id=user_id, page=page, page_size=page_size
)
@staticmethod
async def test_get_preset(preset_id: str, user_id: str):
return await backend.server.v2.library.routes.presets.get_preset(
preset_id=preset_id, user_id=user_id
)
@staticmethod
async def test_create_preset(
preset: backend.server.v2.library.model.CreateLibraryAgentPresetRequest,
user_id: str,
):
return await backend.server.v2.library.routes.presets.create_preset(
preset=preset, user_id=user_id
)
@staticmethod
async def test_update_preset(
preset_id: str,
preset: backend.server.v2.library.model.CreateLibraryAgentPresetRequest,
user_id: str,
):
return await backend.server.v2.library.routes.presets.update_preset(
preset_id=preset_id, preset=preset, user_id=user_id
)
@staticmethod
async def test_delete_preset(preset_id: str, user_id: str):
return await backend.server.v2.library.routes.presets.delete_preset(
preset_id=preset_id, user_id=user_id
)
@staticmethod
async def test_execute_preset(
graph_id: str,
graph_version: int,
preset_id: str,
node_input: dict[typing.Any, typing.Any],
user_id: str,
):
return await backend.server.v2.library.routes.presets.execute_preset(
graph_id=graph_id,
graph_version=graph_version,
preset_id=preset_id,
node_input=node_input,
user_id=user_id,
)
@staticmethod
async def test_create_store_listing(
request: backend.server.v2.store.model.StoreSubmissionRequest, user_id: str
):
return await backend.server.v2.store.routes.create_submission(request, user_id)
@staticmethod
async def test_review_store_listing(
request: backend.server.v2.store.model.ReviewSubmissionRequest,
user: autogpt_libs.auth.models.User,
):
return await backend.server.v2.store.routes.review_submission(request, user)
def set_test_dependency_overrides(self, overrides: dict): def set_test_dependency_overrides(self, overrides: dict):
app.dependency_overrides.update(overrides) app.dependency_overrides.update(overrides)

View File

@ -13,7 +13,6 @@ from typing_extensions import Optional, TypedDict
import backend.data.block import backend.data.block
import backend.server.integrations.router import backend.server.integrations.router
import backend.server.routers.analytics import backend.server.routers.analytics
import backend.server.v2.library.db
from backend.data import execution as execution_db from backend.data import execution as execution_db
from backend.data import graph as graph_db from backend.data import graph as graph_db
from backend.data.api_key import ( from backend.data.api_key import (
@ -181,6 +180,11 @@ async def get_graph(
tags=["graphs"], tags=["graphs"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@v1_router.get(
path="/templates/{graph_id}/versions",
tags=["templates", "graphs"],
dependencies=[Depends(auth_middleware)],
)
async def get_graph_all_versions( async def get_graph_all_versions(
graph_id: str, user_id: Annotated[str, Depends(get_user_id)] graph_id: str, user_id: Annotated[str, Depends(get_user_id)]
) -> Sequence[graph_db.GraphModel]: ) -> Sequence[graph_db.GraphModel]:
@ -196,11 +200,12 @@ async def get_graph_all_versions(
async def create_new_graph( async def create_new_graph(
create_graph: CreateGraph, user_id: Annotated[str, Depends(get_user_id)] create_graph: CreateGraph, user_id: Annotated[str, Depends(get_user_id)]
) -> graph_db.GraphModel: ) -> graph_db.GraphModel:
return await do_create_graph(create_graph, user_id=user_id) return await do_create_graph(create_graph, is_template=False, user_id=user_id)
async def do_create_graph( async def do_create_graph(
create_graph: CreateGraph, create_graph: CreateGraph,
is_template: bool,
# user_id doesn't have to be annotated like on other endpoints, # user_id doesn't have to be annotated like on other endpoints,
# because create_graph isn't used directly as an endpoint # because create_graph isn't used directly as an endpoint
user_id: str, user_id: str,
@ -212,6 +217,7 @@ async def do_create_graph(
graph = await graph_db.get_graph( graph = await graph_db.get_graph(
create_graph.template_id, create_graph.template_id,
create_graph.template_version, create_graph.template_version,
template=True,
user_id=user_id, user_id=user_id,
) )
if not graph: if not graph:
@ -224,17 +230,11 @@ async def do_create_graph(
status_code=400, detail="Either graph or template_id must be provided." status_code=400, detail="Either graph or template_id must be provided."
) )
graph.is_template = is_template
graph.is_active = not is_template
graph.reassign_ids(user_id=user_id, reassign_graph_id=True) graph.reassign_ids(user_id=user_id, reassign_graph_id=True)
graph = await graph_db.create_graph(graph, user_id=user_id) graph = await graph_db.create_graph(graph, user_id=user_id)
# Create a library agent for the new graph
await backend.server.v2.library.db.create_library_agent(
graph.id,
graph.version,
user_id,
)
graph = await on_graph_activate( graph = await on_graph_activate(
graph, graph,
get_credentials=lambda id: integration_creds_manager.get(user_id, id), get_credentials=lambda id: integration_creds_manager.get(user_id, id),
@ -261,6 +261,11 @@ async def delete_graph(
@v1_router.put( @v1_router.put(
path="/graphs/{graph_id}", tags=["graphs"], dependencies=[Depends(auth_middleware)] path="/graphs/{graph_id}", tags=["graphs"], dependencies=[Depends(auth_middleware)]
) )
@v1_router.put(
path="/templates/{graph_id}",
tags=["templates", "graphs"],
dependencies=[Depends(auth_middleware)],
)
async def update_graph( async def update_graph(
graph_id: str, graph_id: str,
graph: graph_db.Graph, graph: graph_db.Graph,
@ -292,10 +297,6 @@ async def update_graph(
new_graph_version = await graph_db.create_graph(graph, user_id=user_id) new_graph_version = await graph_db.create_graph(graph, user_id=user_id)
if new_graph_version.is_active: if new_graph_version.is_active:
# Keep the library agent up to date with the new active version
await backend.server.v2.library.db.update_agent_version_in_library(
user_id, graph.id, graph.version
)
def get_credentials(credentials_id: str) -> "Credentials | None": def get_credentials(credentials_id: str) -> "Credentials | None":
return integration_creds_manager.get(user_id, credentials_id) return integration_creds_manager.get(user_id, credentials_id)
@ -352,12 +353,6 @@ async def set_graph_active_version(
version=new_active_version, version=new_active_version,
user_id=user_id, user_id=user_id,
) )
# Keep the library agent up to date with the new active version
await backend.server.v2.library.db.update_agent_version_in_library(
user_id, new_active_graph.id, new_active_graph.version
)
if current_active_graph and current_active_graph.version != new_active_version: if current_active_graph and current_active_graph.version != new_active_version:
# Handle deactivation of the previously active version # Handle deactivation of the previously active version
await on_graph_deactivate( await on_graph_deactivate(
@ -375,11 +370,10 @@ def execute_graph(
graph_id: str, graph_id: str,
node_input: dict[Any, Any], node_input: dict[Any, Any],
user_id: Annotated[str, Depends(get_user_id)], user_id: Annotated[str, Depends(get_user_id)],
graph_version: Optional[int] = None,
) -> dict[str, Any]: # FIXME: add proper return type ) -> dict[str, Any]: # FIXME: add proper return type
try: try:
graph_exec = execution_manager_client().add_execution( graph_exec = execution_manager_client().add_execution(
graph_id, node_input, user_id=user_id, graph_version=graph_version graph_id, node_input, user_id=user_id
) )
return {"id": graph_exec.graph_exec_id} return {"id": graph_exec.graph_exec_id}
except Exception as e: except Exception as e:
@ -394,10 +388,8 @@ def execute_graph(
) )
async def stop_graph_run( async def stop_graph_run(
graph_exec_id: str, user_id: Annotated[str, Depends(get_user_id)] graph_exec_id: str, user_id: Annotated[str, Depends(get_user_id)]
) -> graph_db.GraphExecution: ) -> Sequence[execution_db.ExecutionResult]:
if not await graph_db.get_execution_meta( if not await graph_db.get_execution(user_id=user_id, execution_id=graph_exec_id):
user_id=user_id, execution_id=graph_exec_id
):
raise HTTPException(404, detail=f"Agent execution #{graph_exec_id} not found") raise HTTPException(404, detail=f"Agent execution #{graph_exec_id} not found")
await asyncio.to_thread( await asyncio.to_thread(
@ -405,13 +397,7 @@ async def stop_graph_run(
) )
# Retrieve & return canceled graph execution in its final state # Retrieve & return canceled graph execution in its final state
result = await graph_db.get_execution(execution_id=graph_exec_id, user_id=user_id) return await execution_db.get_execution_results(graph_exec_id)
if not result:
raise HTTPException(
500,
detail=f"Could not fetch graph execution #{graph_exec_id} after stopping",
)
return result
@v1_router.get( @v1_router.get(
@ -419,22 +405,10 @@ async def stop_graph_run(
tags=["graphs"], tags=["graphs"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
async def get_graphs_executions( async def get_executions(
user_id: Annotated[str, Depends(get_user_id)], user_id: Annotated[str, Depends(get_user_id)],
) -> list[graph_db.GraphExecutionMeta]: ) -> list[graph_db.GraphExecution]:
return await graph_db.get_graphs_executions(user_id=user_id) return await graph_db.get_executions(user_id=user_id)
@v1_router.get(
path="/graphs/{graph_id}/executions",
tags=["graphs"],
dependencies=[Depends(auth_middleware)],
)
async def get_graph_executions(
graph_id: str,
user_id: Annotated[str, Depends(get_user_id)],
) -> list[graph_db.GraphExecutionMeta]:
return await graph_db.get_graph_executions(graph_id=graph_id, user_id=user_id)
@v1_router.get( @v1_router.get(
@ -442,20 +416,57 @@ async def get_graph_executions(
tags=["graphs"], tags=["graphs"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
async def get_graph_execution( async def get_graph_run_node_execution_results(
graph_id: str, graph_id: str,
graph_exec_id: str, graph_exec_id: str,
user_id: Annotated[str, Depends(get_user_id)], user_id: Annotated[str, Depends(get_user_id)],
) -> graph_db.GraphExecution: ) -> Sequence[execution_db.ExecutionResult]:
graph = await graph_db.get_graph(graph_id, user_id=user_id) graph = await graph_db.get_graph(graph_id, user_id=user_id)
if not graph: if not graph:
raise HTTPException(status_code=404, detail=f"Graph #{graph_id} not found.") raise HTTPException(status_code=404, detail=f"Graph #{graph_id} not found.")
result = await graph_db.get_execution(execution_id=graph_exec_id, user_id=user_id) return await execution_db.get_execution_results(graph_exec_id)
if not result:
raise HTTPException(status_code=404, detail=f"Graph #{graph_id} not found.")
return result
########################################################
##################### Templates ########################
########################################################
@v1_router.get(
path="/templates",
tags=["graphs", "templates"],
dependencies=[Depends(auth_middleware)],
)
async def get_templates(
user_id: Annotated[str, Depends(get_user_id)]
) -> Sequence[graph_db.GraphModel]:
return await graph_db.get_graphs(filter_by="template", user_id=user_id)
@v1_router.get(
path="/templates/{graph_id}",
tags=["templates", "graphs"],
dependencies=[Depends(auth_middleware)],
)
async def get_template(
graph_id: str, version: int | None = None
) -> graph_db.GraphModel:
graph = await graph_db.get_graph(graph_id, version, template=True)
if not graph:
raise HTTPException(status_code=404, detail=f"Template #{graph_id} not found.")
return graph
@v1_router.post(
path="/templates",
tags=["templates", "graphs"],
dependencies=[Depends(auth_middleware)],
)
async def create_new_template(
create_graph: CreateGraph, user_id: Annotated[str, Depends(get_user_id)]
) -> graph_db.GraphModel:
return await do_create_graph(create_graph, is_template=True, user_id=user_id)
######################################################## ########################################################

View File

@ -1,5 +1,5 @@
import json
import logging import logging
from typing import List
import prisma.errors import prisma.errors
import prisma.models import prisma.models
@ -7,7 +7,6 @@ import prisma.types
import backend.data.graph import backend.data.graph
import backend.data.includes import backend.data.includes
import backend.server.model
import backend.server.v2.library.model import backend.server.v2.library.model
import backend.server.v2.store.exceptions import backend.server.v2.store.exceptions
@ -15,167 +14,90 @@ logger = logging.getLogger(__name__)
async def get_library_agents( async def get_library_agents(
user_id: str, search_query: str | None = None user_id: str,
) -> list[backend.server.v2.library.model.LibraryAgent]: ) -> List[backend.server.v2.library.model.LibraryAgent]:
logger.debug( """
f"Fetching library agents for user_id={user_id} search_query={search_query}" Returns all agents (AgentGraph) that belong to the user and all agents in their library (UserAgent table)
) """
logger.debug(f"Getting library agents for user {user_id}")
if search_query and len(search_query.strip()) > 100:
logger.warning(f"Search query too long: {search_query}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Search query is too long."
)
where_clause: prisma.types.LibraryAgentWhereInput = {
"userId": user_id,
"isDeleted": False,
"isArchived": False,
}
if search_query:
where_clause["OR"] = [
{
"Agent": {
"is": {"name": {"contains": search_query, "mode": "insensitive"}}
}
},
{
"Agent": {
"is": {
"description": {"contains": search_query, "mode": "insensitive"}
}
}
},
]
try: try:
library_agents = await prisma.models.LibraryAgent.prisma().find_many( # Get agents created by user with nodes and links
where=where_clause, user_created = await prisma.models.AgentGraph.prisma().find_many(
where=prisma.types.AgentGraphWhereInput(userId=user_id, isActive=True),
include=backend.data.includes.AGENT_GRAPH_INCLUDE,
)
# Get agents in user's library with nodes and links
library_agents = await prisma.models.UserAgent.prisma().find_many(
where=prisma.types.UserAgentWhereInput(
userId=user_id, isDeleted=False, isArchived=False
),
include={ include={
"Agent": { "Agent": {
"include": { "include": {
"AgentNodes": {"include": {"Input": True, "Output": True}} "AgentNodes": {
"include": {
"Input": True,
"Output": True,
"Webhook": True,
"AgentBlock": True,
}
}
} }
} }
}, },
order=[{"updatedAt": "desc"}],
) )
logger.debug(f"Retrieved {len(library_agents)} agents for user_id={user_id}.")
return [ # Convert to Graph models first
backend.server.v2.library.model.LibraryAgent.from_db(agent) graphs = []
for agent in library_agents
] # Add user created agents
for agent in user_created:
try:
graphs.append(backend.data.graph.GraphModel.from_db(agent))
except Exception as e:
logger.error(f"Error processing user created agent {agent.id}: {e}")
continue
# Add library agents
for agent in library_agents:
if agent.Agent:
try:
graphs.append(backend.data.graph.GraphModel.from_db(agent.Agent))
except Exception as e:
logger.error(f"Error processing library agent {agent.agentId}: {e}")
continue
# Convert Graph models to LibraryAgent models
result = []
for graph in graphs:
result.append(
backend.server.v2.library.model.LibraryAgent(
id=graph.id,
version=graph.version,
is_active=graph.is_active,
name=graph.name,
description=graph.description,
isCreatedByUser=any(a.id == graph.id for a in user_created),
input_schema=graph.input_schema,
output_schema=graph.output_schema,
)
)
logger.debug(f"Found {len(result)} library agents")
return result
except prisma.errors.PrismaError as e: except prisma.errors.PrismaError as e:
logger.error(f"Database error fetching library agents: {e}") logger.error(f"Database error getting library agents: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError( raise backend.server.v2.store.exceptions.DatabaseError(
"Unable to fetch library agents." "Failed to fetch library agents"
)
async def create_library_agent(
agent_id: str, agent_version: int, user_id: str
) -> prisma.models.LibraryAgent:
"""
Adds an agent to the user's library (LibraryAgent table)
"""
try:
return await prisma.models.LibraryAgent.prisma().create(
data={
"userId": user_id,
"agentId": agent_id,
"agentVersion": agent_version,
"isCreatedByUser": False,
"useGraphIsActiveVersion": True,
}
)
except prisma.errors.PrismaError as e:
logger.error(f"Database error creating agent to library: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to create agent to library"
) from e ) from e
async def update_agent_version_in_library( async def add_agent_to_library(store_listing_version_id: str, user_id: str) -> None:
user_id: str, agent_id: str, agent_version: int
) -> None:
""" """
Updates the agent version in the library Finds the agent from the store listing version and adds it to the user's library (UserAgent table)
"""
try:
library_agent = await prisma.models.LibraryAgent.prisma().find_first_or_raise(
where={
"userId": user_id,
"agentId": agent_id,
"useGraphIsActiveVersion": True,
},
)
await prisma.models.LibraryAgent.prisma().update(
where={"id": library_agent.id},
data={
"Agent": {
"connect": {
"graphVersionId": {"id": agent_id, "version": agent_version}
},
},
},
)
except prisma.errors.PrismaError as e:
logger.error(f"Database error updating agent version in library: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to update agent version in library"
) from e
async def update_library_agent(
library_agent_id: str,
user_id: str,
auto_update_version: bool = False,
is_favorite: bool = False,
is_archived: bool = False,
is_deleted: bool = False,
) -> None:
"""
Updates the library agent with the given fields
"""
try:
await prisma.models.LibraryAgent.prisma().update_many(
where={"id": library_agent_id, "userId": user_id},
data={
"useGraphIsActiveVersion": auto_update_version,
"isFavorite": is_favorite,
"isArchived": is_archived,
"isDeleted": is_deleted,
},
)
except prisma.errors.PrismaError as e:
logger.error(f"Database error updating library agent: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to update library agent"
) from e
async def delete_library_agent_by_graph_id(graph_id: str, user_id: str) -> None:
"""
Deletes a library agent for the given user
"""
try:
await prisma.models.LibraryAgent.prisma().delete_many(
where={"agentId": graph_id, "userId": user_id}
)
except prisma.errors.PrismaError as e:
logger.error(f"Database error deleting library agent: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to delete library agent"
) from e
async def add_store_agent_to_library(
store_listing_version_id: str, user_id: str
) -> None:
"""
Finds the agent from the store listing version and adds it to the user's library (LibraryAgent table)
if they don't already have it if they don't already have it
""" """
logger.debug( logger.debug(
@ -209,7 +131,7 @@ async def add_store_agent_to_library(
) )
# Check if user already has this agent # Check if user already has this agent
existing_user_agent = await prisma.models.LibraryAgent.prisma().find_first( existing_user_agent = await prisma.models.UserAgent.prisma().find_first(
where={ where={
"userId": user_id, "userId": user_id,
"agentId": agent.id, "agentId": agent.id,
@ -223,14 +145,14 @@ async def add_store_agent_to_library(
) )
return return
# Create LibraryAgent entry # Create UserAgent entry
await prisma.models.LibraryAgent.prisma().create( await prisma.models.UserAgent.prisma().create(
data={ data=prisma.types.UserAgentCreateInput(
"userId": user_id, userId=user_id,
"agentId": agent.id, agentId=agent.id,
"agentVersion": agent.version, agentVersion=agent.version,
"isCreatedByUser": False, isCreatedByUser=False,
} )
) )
logger.debug(f"Added agent {agent.id} to library for user {user_id}") logger.debug(f"Added agent {agent.id} to library for user {user_id}")
@ -241,127 +163,3 @@ async def add_store_agent_to_library(
raise backend.server.v2.store.exceptions.DatabaseError( raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to add agent to library" "Failed to add agent to library"
) from e ) from e
##############################################
########### Presets DB Functions #############
##############################################
async def get_presets(
user_id: str, page: int, page_size: int
) -> backend.server.v2.library.model.LibraryAgentPresetResponse:
try:
presets = await prisma.models.AgentPreset.prisma().find_many(
where={"userId": user_id},
skip=page * page_size,
take=page_size,
)
total_items = await prisma.models.AgentPreset.prisma().count(
where={"userId": user_id},
)
total_pages = (total_items + page_size - 1) // page_size
presets = [
backend.server.v2.library.model.LibraryAgentPreset.from_db(preset)
for preset in presets
]
return backend.server.v2.library.model.LibraryAgentPresetResponse(
presets=presets,
pagination=backend.server.model.Pagination(
total_items=total_items,
total_pages=total_pages,
current_page=page,
page_size=page_size,
),
)
except prisma.errors.PrismaError as e:
logger.error(f"Database error getting presets: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch presets"
) from e
async def get_preset(
user_id: str, preset_id: str
) -> backend.server.v2.library.model.LibraryAgentPreset | None:
try:
preset = await prisma.models.AgentPreset.prisma().find_unique(
where={"id": preset_id}, include={"InputPresets": True}
)
if not preset or preset.userId != user_id:
return None
return backend.server.v2.library.model.LibraryAgentPreset.from_db(preset)
except prisma.errors.PrismaError as e:
logger.error(f"Database error getting preset: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch preset"
) from e
async def create_or_update_preset(
user_id: str,
preset: backend.server.v2.library.model.CreateLibraryAgentPresetRequest,
preset_id: str | None = None,
) -> backend.server.v2.library.model.LibraryAgentPreset:
try:
if preset_id:
# Update existing preset
new_preset = await prisma.models.AgentPreset.prisma().update(
where={"id": preset_id},
data={
"name": preset.name,
"description": preset.description,
"isActive": preset.is_active,
"InputPresets": {
"create": [
{"name": name, "data": json.dumps(data)}
for name, data in preset.inputs.items()
]
},
},
include={"InputPresets": True},
)
if not new_preset:
raise ValueError(f"AgentPreset #{preset_id} not found")
else:
# Create new preset
new_preset = await prisma.models.AgentPreset.prisma().create(
data={
"userId": user_id,
"name": preset.name,
"description": preset.description,
"agentId": preset.agent_id,
"agentVersion": preset.agent_version,
"isActive": preset.is_active,
"InputPresets": {
"create": [
{"name": name, "data": json.dumps(data)}
for name, data in preset.inputs.items()
]
},
},
include={"InputPresets": True},
)
return backend.server.v2.library.model.LibraryAgentPreset.from_db(new_preset)
except prisma.errors.PrismaError as e:
logger.error(f"Database error creating preset: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to create preset"
) from e
async def delete_preset(user_id: str, preset_id: str) -> None:
try:
await prisma.models.AgentPreset.prisma().update_many(
where={"id": preset_id, "userId": user_id},
data={"isDeleted": True},
)
except prisma.errors.PrismaError as e:
logger.error(f"Database error deleting preset: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to delete preset"
) from e

View File

@ -37,7 +37,7 @@ async def test_get_library_agents(mocker):
] ]
mock_library_agents = [ mock_library_agents = [
prisma.models.LibraryAgent( prisma.models.UserAgent(
id="ua1", id="ua1",
userId="test-user", userId="test-user",
agentId="agent2", agentId="agent2",
@ -48,7 +48,6 @@ async def test_get_library_agents(mocker):
createdAt=datetime.now(), createdAt=datetime.now(),
updatedAt=datetime.now(), updatedAt=datetime.now(),
isFavorite=False, isFavorite=False,
useGraphIsActiveVersion=True,
Agent=prisma.models.AgentGraph( Agent=prisma.models.AgentGraph(
id="agent2", id="agent2",
version=1, version=1,
@ -68,8 +67,8 @@ async def test_get_library_agents(mocker):
return_value=mock_user_created return_value=mock_user_created
) )
mock_library_agent = mocker.patch("prisma.models.LibraryAgent.prisma") mock_user_agent = mocker.patch("prisma.models.UserAgent.prisma")
mock_library_agent.return_value.find_many = mocker.AsyncMock( mock_user_agent.return_value.find_many = mocker.AsyncMock(
return_value=mock_library_agents return_value=mock_library_agents
) )
@ -77,16 +76,40 @@ async def test_get_library_agents(mocker):
result = await db.get_library_agents("test-user") result = await db.get_library_agents("test-user")
# Verify results # Verify results
assert len(result) == 1 assert len(result) == 2
assert result[0].id == "ua1" assert result[0].id == "agent1"
assert result[0].name == "Test Agent 2" assert result[0].name == "Test Agent 1"
assert result[0].description == "Test Description 2" assert result[0].description == "Test Description 1"
assert result[0].is_created_by_user is False assert result[0].isCreatedByUser is True
assert result[0].is_latest_version is True assert result[1].id == "agent2"
assert result[0].is_favorite is False assert result[1].name == "Test Agent 2"
assert result[0].agent_id == "agent2" assert result[1].description == "Test Description 2"
assert result[0].agent_version == 1 assert result[1].isCreatedByUser is False
assert result[0].preset_id is None
# Verify mocks called correctly
mock_agent_graph.return_value.find_many.assert_called_once_with(
where=prisma.types.AgentGraphWhereInput(userId="test-user", isActive=True),
include=backend.data.includes.AGENT_GRAPH_INCLUDE,
)
mock_user_agent.return_value.find_many.assert_called_once_with(
where=prisma.types.UserAgentWhereInput(
userId="test-user", isDeleted=False, isArchived=False
),
include={
"Agent": {
"include": {
"AgentNodes": {
"include": {
"Input": True,
"Output": True,
"Webhook": True,
"AgentBlock": True,
}
}
}
}
},
)
@pytest.mark.asyncio @pytest.mark.asyncio
@ -129,26 +152,26 @@ async def test_add_agent_to_library(mocker):
return_value=mock_store_listing return_value=mock_store_listing
) )
mock_library_agent = mocker.patch("prisma.models.LibraryAgent.prisma") mock_user_agent = mocker.patch("prisma.models.UserAgent.prisma")
mock_library_agent.return_value.find_first = mocker.AsyncMock(return_value=None) mock_user_agent.return_value.find_first = mocker.AsyncMock(return_value=None)
mock_library_agent.return_value.create = mocker.AsyncMock() mock_user_agent.return_value.create = mocker.AsyncMock()
# Call function # Call function
await db.add_store_agent_to_library("version123", "test-user") await db.add_agent_to_library("version123", "test-user")
# Verify mocks called correctly # Verify mocks called correctly
mock_store_listing_version.return_value.find_unique.assert_called_once_with( mock_store_listing_version.return_value.find_unique.assert_called_once_with(
where={"id": "version123"}, include={"Agent": True} where={"id": "version123"}, include={"Agent": True}
) )
mock_library_agent.return_value.find_first.assert_called_once_with( mock_user_agent.return_value.find_first.assert_called_once_with(
where={ where={
"userId": "test-user", "userId": "test-user",
"agentId": "agent1", "agentId": "agent1",
"agentVersion": 1, "agentVersion": 1,
} }
) )
mock_library_agent.return_value.create.assert_called_once_with( mock_user_agent.return_value.create.assert_called_once_with(
data=prisma.types.LibraryAgentCreateInput( data=prisma.types.UserAgentCreateInput(
userId="test-user", agentId="agent1", agentVersion=1, isCreatedByUser=False userId="test-user", agentId="agent1", agentVersion=1, isCreatedByUser=False
) )
) )
@ -166,7 +189,7 @@ async def test_add_agent_to_library_not_found(mocker):
# Call function and verify exception # Call function and verify exception
with pytest.raises(backend.server.v2.store.exceptions.AgentNotFoundError): with pytest.raises(backend.server.v2.store.exceptions.AgentNotFoundError):
await db.add_store_agent_to_library("version123", "test-user") await db.add_agent_to_library("version123", "test-user")
# Verify mock called correctly # Verify mock called correctly
mock_store_listing_version.return_value.find_unique.assert_called_once_with( mock_store_listing_version.return_value.find_unique.assert_called_once_with(

View File

@ -1,112 +1,16 @@
import datetime
import json
import typing import typing
import prisma.models
import pydantic import pydantic
import backend.data.block
import backend.data.graph
import backend.server.model
class LibraryAgent(pydantic.BaseModel): class LibraryAgent(pydantic.BaseModel):
id: str # Changed from agent_id to match GraphMeta id: str # Changed from agent_id to match GraphMeta
version: int # Changed from agent_version to match GraphMeta
agent_id: str is_active: bool # Added to match GraphMeta
agent_version: int # Changed from agent_version to match GraphMeta
preset_id: str | None
updated_at: datetime.datetime
name: str name: str
description: str description: str
isCreatedByUser: bool
# Made input_schema and output_schema match GraphMeta's type # Made input_schema and output_schema match GraphMeta's type
input_schema: dict[str, typing.Any] # Should be BlockIOObjectSubSchema in frontend input_schema: dict[str, typing.Any] # Should be BlockIOObjectSubSchema in frontend
output_schema: dict[str, typing.Any] # Should be BlockIOObjectSubSchema in frontend output_schema: dict[str, typing.Any] # Should be BlockIOObjectSubSchema in frontend
is_favorite: bool
is_created_by_user: bool
is_latest_version: bool
@staticmethod
def from_db(agent: prisma.models.LibraryAgent):
if not agent.Agent:
raise ValueError("AgentGraph is required")
graph = backend.data.graph.GraphModel.from_db(agent.Agent)
agent_updated_at = agent.Agent.updatedAt
lib_agent_updated_at = agent.updatedAt
# Take the latest updated_at timestamp either when the graph was updated or the library agent was updated
updated_at = (
max(agent_updated_at, lib_agent_updated_at)
if agent_updated_at
else lib_agent_updated_at
)
return LibraryAgent(
id=agent.id,
agent_id=agent.agentId,
agent_version=agent.agentVersion,
updated_at=updated_at,
name=graph.name,
description=graph.description,
input_schema=graph.input_schema,
output_schema=graph.output_schema,
is_favorite=agent.isFavorite,
is_created_by_user=agent.isCreatedByUser,
is_latest_version=graph.is_active,
preset_id=agent.AgentPreset.id if agent.AgentPreset else None,
)
class LibraryAgentPreset(pydantic.BaseModel):
id: str
updated_at: datetime.datetime
agent_id: str
agent_version: int
name: str
description: str
is_active: bool
inputs: dict[str, typing.Union[backend.data.block.BlockInput, typing.Any]]
@staticmethod
def from_db(preset: prisma.models.AgentPreset):
input_data = {}
for data in preset.InputPresets or []:
input_data[data.name] = json.loads(data.data)
return LibraryAgentPreset(
id=preset.id,
updated_at=preset.updatedAt,
agent_id=preset.agentId,
agent_version=preset.agentVersion,
name=preset.name,
description=preset.description,
is_active=preset.isActive,
inputs=input_data,
)
class LibraryAgentPresetResponse(pydantic.BaseModel):
presets: list[LibraryAgentPreset]
pagination: backend.server.model.Pagination
class CreateLibraryAgentPresetRequest(pydantic.BaseModel):
name: str
description: str
inputs: dict[str, typing.Union[backend.data.block.BlockInput, typing.Any]]
agent_id: str
agent_version: int
is_active: bool

View File

@ -1,35 +1,23 @@
import datetime
import prisma.models
import backend.data.block
import backend.server.model
import backend.server.v2.library.model import backend.server.v2.library.model
def test_library_agent(): def test_library_agent():
agent = backend.server.v2.library.model.LibraryAgent( agent = backend.server.v2.library.model.LibraryAgent(
id="test-agent-123", id="test-agent-123",
agent_id="agent-123", version=1,
agent_version=1, is_active=True,
preset_id=None,
updated_at=datetime.datetime.now(),
name="Test Agent", name="Test Agent",
description="Test description", description="Test description",
isCreatedByUser=False,
input_schema={"type": "object", "properties": {}}, input_schema={"type": "object", "properties": {}},
output_schema={"type": "object", "properties": {}}, output_schema={"type": "object", "properties": {}},
is_favorite=False,
is_created_by_user=False,
is_latest_version=True,
) )
assert agent.id == "test-agent-123" assert agent.id == "test-agent-123"
assert agent.agent_id == "agent-123" assert agent.version == 1
assert agent.agent_version == 1 assert agent.is_active is True
assert agent.name == "Test Agent" assert agent.name == "Test Agent"
assert agent.description == "Test description" assert agent.description == "Test description"
assert agent.is_favorite is False assert agent.isCreatedByUser is False
assert agent.is_created_by_user is False
assert agent.is_latest_version is True
assert agent.input_schema == {"type": "object", "properties": {}} assert agent.input_schema == {"type": "object", "properties": {}}
assert agent.output_schema == {"type": "object", "properties": {}} assert agent.output_schema == {"type": "object", "properties": {}}
@ -37,148 +25,19 @@ def test_library_agent():
def test_library_agent_with_user_created(): def test_library_agent_with_user_created():
agent = backend.server.v2.library.model.LibraryAgent( agent = backend.server.v2.library.model.LibraryAgent(
id="user-agent-456", id="user-agent-456",
agent_id="agent-456", version=2,
agent_version=2, is_active=True,
preset_id=None,
updated_at=datetime.datetime.now(),
name="User Created Agent", name="User Created Agent",
description="An agent created by the user", description="An agent created by the user",
isCreatedByUser=True,
input_schema={"type": "object", "properties": {}}, input_schema={"type": "object", "properties": {}},
output_schema={"type": "object", "properties": {}}, output_schema={"type": "object", "properties": {}},
is_favorite=False,
is_created_by_user=True,
is_latest_version=True,
) )
assert agent.id == "user-agent-456" assert agent.id == "user-agent-456"
assert agent.agent_id == "agent-456" assert agent.version == 2
assert agent.agent_version == 2 assert agent.is_active is True
assert agent.name == "User Created Agent" assert agent.name == "User Created Agent"
assert agent.description == "An agent created by the user" assert agent.description == "An agent created by the user"
assert agent.is_favorite is False assert agent.isCreatedByUser is True
assert agent.is_created_by_user is True
assert agent.is_latest_version is True
assert agent.input_schema == {"type": "object", "properties": {}} assert agent.input_schema == {"type": "object", "properties": {}}
assert agent.output_schema == {"type": "object", "properties": {}} assert agent.output_schema == {"type": "object", "properties": {}}
def test_library_agent_preset():
preset = backend.server.v2.library.model.LibraryAgentPreset(
id="preset-123",
name="Test Preset",
description="Test preset description",
agent_id="test-agent-123",
agent_version=1,
is_active=True,
inputs={
"input1": backend.data.block.BlockInput(
name="input1",
data={"type": "string", "value": "test value"},
)
},
updated_at=datetime.datetime.now(),
)
assert preset.id == "preset-123"
assert preset.name == "Test Preset"
assert preset.description == "Test preset description"
assert preset.agent_id == "test-agent-123"
assert preset.agent_version == 1
assert preset.is_active is True
assert preset.inputs == {
"input1": backend.data.block.BlockInput(
name="input1", data={"type": "string", "value": "test value"}
)
}
def test_library_agent_preset_response():
preset = backend.server.v2.library.model.LibraryAgentPreset(
id="preset-123",
name="Test Preset",
description="Test preset description",
agent_id="test-agent-123",
agent_version=1,
is_active=True,
inputs={
"input1": backend.data.block.BlockInput(
name="input1",
data={"type": "string", "value": "test value"},
)
},
updated_at=datetime.datetime.now(),
)
pagination = backend.server.model.Pagination(
total_items=1, total_pages=1, current_page=1, page_size=10
)
response = backend.server.v2.library.model.LibraryAgentPresetResponse(
presets=[preset], pagination=pagination
)
assert len(response.presets) == 1
assert response.presets[0].id == "preset-123"
assert response.pagination.total_items == 1
assert response.pagination.total_pages == 1
assert response.pagination.current_page == 1
assert response.pagination.page_size == 10
def test_create_library_agent_preset_request():
request = backend.server.v2.library.model.CreateLibraryAgentPresetRequest(
name="New Preset",
description="New preset description",
agent_id="agent-123",
agent_version=1,
is_active=True,
inputs={
"input1": backend.data.block.BlockInput(
name="input1",
data={"type": "string", "value": "test value"},
)
},
)
assert request.name == "New Preset"
assert request.description == "New preset description"
assert request.agent_id == "agent-123"
assert request.agent_version == 1
assert request.is_active is True
assert request.inputs == {
"input1": backend.data.block.BlockInput(
name="input1", data={"type": "string", "value": "test value"}
)
}
def test_library_agent_from_db():
# Create mock DB agent
db_agent = prisma.models.AgentPreset(
id="test-agent-123",
createdAt=datetime.datetime.now(),
updatedAt=datetime.datetime.now(),
agentId="agent-123",
agentVersion=1,
name="Test Agent",
description="Test agent description",
isActive=True,
userId="test-user-123",
isDeleted=False,
InputPresets=[
prisma.models.AgentNodeExecutionInputOutput(
id="input-123",
time=datetime.datetime.now(),
name="input1",
data='{"type": "string", "value": "test value"}',
)
],
)
# Convert to LibraryAgentPreset
agent = backend.server.v2.library.model.LibraryAgentPreset.from_db(db_agent)
assert agent.id == "test-agent-123"
assert agent.agent_version == 1
assert agent.is_active is True
assert agent.name == "Test Agent"
assert agent.description == "Test agent description"
assert agent.inputs == {"input1": {"type": "string", "value": "test value"}}

View File

@ -0,0 +1,123 @@
import logging
import typing
import autogpt_libs.auth.depends
import autogpt_libs.auth.middleware
import fastapi
import prisma
import backend.data.graph
import backend.integrations.creds_manager
import backend.integrations.webhooks.graph_lifecycle_hooks
import backend.server.v2.library.db
import backend.server.v2.library.model
logger = logging.getLogger(__name__)
router = fastapi.APIRouter()
integration_creds_manager = (
backend.integrations.creds_manager.IntegrationCredentialsManager()
)
@router.get(
"/agents",
tags=["library", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
)
async def get_library_agents(
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
]
) -> typing.Sequence[backend.server.v2.library.model.LibraryAgent]:
"""
Get all agents in the user's library, including both created and saved agents.
"""
try:
agents = await backend.server.v2.library.db.get_library_agents(user_id)
return agents
except Exception:
logger.exception("Exception occurred whilst getting library agents")
raise fastapi.HTTPException(
status_code=500, detail="Failed to get library agents"
)
@router.post(
"/agents/{store_listing_version_id}",
tags=["library", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
status_code=201,
)
async def add_agent_to_library(
store_listing_version_id: str,
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
) -> fastapi.Response:
"""
Add an agent from the store to the user's library.
Args:
store_listing_version_id (str): ID of the store listing version to add
user_id (str): ID of the authenticated user
Returns:
fastapi.Response: 201 status code on success
Raises:
HTTPException: If there is an error adding the agent to the library
"""
try:
# Get the graph from the store listing
store_listing_version = (
await prisma.models.StoreListingVersion.prisma().find_unique(
where={"id": store_listing_version_id}, include={"Agent": True}
)
)
if not store_listing_version or not store_listing_version.Agent:
raise fastapi.HTTPException(
status_code=404,
detail=f"Store listing version {store_listing_version_id} not found",
)
agent = store_listing_version.Agent
if agent.userId == user_id:
raise fastapi.HTTPException(
status_code=400, detail="Cannot add own agent to library"
)
# Create a new graph from the template
graph = await backend.data.graph.get_graph(
agent.id, agent.version, template=True, user_id=user_id
)
if not graph:
raise fastapi.HTTPException(
status_code=404, detail=f"Agent {agent.id} not found"
)
# Create a deep copy with new IDs
graph.version = 1
graph.is_template = False
graph.is_active = True
graph.reassign_ids(user_id=user_id, reassign_graph_id=True)
# Save the new graph
graph = await backend.data.graph.create_graph(graph, user_id=user_id)
graph = (
await backend.integrations.webhooks.graph_lifecycle_hooks.on_graph_activate(
graph,
get_credentials=lambda id: integration_creds_manager.get(user_id, id),
)
)
return fastapi.Response(status_code=201)
except Exception:
logger.exception("Exception occurred whilst adding agent to library")
raise fastapi.HTTPException(
status_code=500, detail="Failed to add agent to library"
)

View File

@ -1,9 +0,0 @@
import fastapi
from .agents import router as agents_router
from .presets import router as presets_router
router = fastapi.APIRouter()
router.include_router(presets_router)
router.include_router(agents_router)

View File

@ -1,148 +0,0 @@
import logging
import typing
import autogpt_libs.auth.depends
import autogpt_libs.auth.middleware
import autogpt_libs.utils.cache
import fastapi
import backend.server.v2.library.db
import backend.server.v2.library.model
import backend.server.v2.store.exceptions
logger = logging.getLogger(__name__)
router = fastapi.APIRouter()
@router.get(
"/agents",
tags=["library", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
)
async def get_library_agents(
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
]
) -> typing.Sequence[backend.server.v2.library.model.LibraryAgent]:
"""
Get all agents in the user's library, including both created and saved agents.
"""
try:
agents = await backend.server.v2.library.db.get_library_agents(user_id)
return agents
except Exception as e:
logger.exception(f"Exception occurred whilst getting library agents: {e}")
raise fastapi.HTTPException(
status_code=500, detail="Failed to get library agents"
)
@router.post(
"/agents/{store_listing_version_id}",
tags=["library", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
status_code=201,
)
async def add_agent_to_library(
store_listing_version_id: str,
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
) -> fastapi.Response:
"""
Add an agent from the store to the user's library.
Args:
store_listing_version_id (str): ID of the store listing version to add
user_id (str): ID of the authenticated user
Returns:
fastapi.Response: 201 status code on success
Raises:
HTTPException: If there is an error adding the agent to the library
"""
try:
# Use the database function to add the agent to the library
await backend.server.v2.library.db.add_store_agent_to_library(
store_listing_version_id, user_id
)
return fastapi.Response(status_code=201)
except backend.server.v2.store.exceptions.AgentNotFoundError:
raise fastapi.HTTPException(
status_code=404,
detail=f"Store listing version {store_listing_version_id} not found",
)
except backend.server.v2.store.exceptions.DatabaseError as e:
logger.exception(f"Database error occurred whilst adding agent to library: {e}")
raise fastapi.HTTPException(
status_code=500, detail="Failed to add agent to library"
)
except Exception as e:
logger.exception(
f"Unexpected exception occurred whilst adding agent to library: {e}"
)
raise fastapi.HTTPException(
status_code=500, detail="Failed to add agent to library"
)
@router.put(
"/agents/{library_agent_id}",
tags=["library", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
status_code=204,
)
async def update_library_agent(
library_agent_id: str,
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
auto_update_version: bool = False,
is_favorite: bool = False,
is_archived: bool = False,
is_deleted: bool = False,
) -> fastapi.Response:
"""
Update the library agent with the given fields.
Args:
library_agent_id (str): ID of the library agent to update
user_id (str): ID of the authenticated user
auto_update_version (bool): Whether to auto-update the agent version
is_favorite (bool): Whether the agent is marked as favorite
is_archived (bool): Whether the agent is archived
is_deleted (bool): Whether the agent is deleted
Returns:
fastapi.Response: 204 status code on success
Raises:
HTTPException: If there is an error updating the library agent
"""
try:
# Use the database function to update the library agent
await backend.server.v2.library.db.update_library_agent(
library_agent_id,
user_id,
auto_update_version,
is_favorite,
is_archived,
is_deleted,
)
return fastapi.Response(status_code=204)
except backend.server.v2.store.exceptions.DatabaseError as e:
logger.exception(f"Database error occurred whilst updating library agent: {e}")
raise fastapi.HTTPException(
status_code=500, detail="Failed to update library agent"
)
except Exception as e:
logger.exception(
f"Unexpected exception occurred whilst updating library agent: {e}"
)
raise fastapi.HTTPException(
status_code=500, detail="Failed to update library agent"
)

View File

@ -1,156 +0,0 @@
import logging
import typing
import autogpt_libs.auth.depends
import autogpt_libs.auth.middleware
import autogpt_libs.utils.cache
import fastapi
import backend.data.graph
import backend.executor
import backend.integrations.creds_manager
import backend.integrations.webhooks.graph_lifecycle_hooks
import backend.server.v2.library.db
import backend.server.v2.library.model
import backend.util.service
logger = logging.getLogger(__name__)
router = fastapi.APIRouter()
integration_creds_manager = (
backend.integrations.creds_manager.IntegrationCredentialsManager()
)
@autogpt_libs.utils.cache.thread_cached
def execution_manager_client() -> backend.executor.ExecutionManager:
return backend.util.service.get_service_client(backend.executor.ExecutionManager)
@router.get("/presets")
async def get_presets(
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
page: int = 1,
page_size: int = 10,
) -> backend.server.v2.library.model.LibraryAgentPresetResponse:
try:
presets = await backend.server.v2.library.db.get_presets(
user_id, page, page_size
)
return presets
except Exception as e:
logger.exception(f"Exception occurred whilst getting presets: {e}")
raise fastapi.HTTPException(status_code=500, detail="Failed to get presets")
@router.get("/presets/{preset_id}")
async def get_preset(
preset_id: str,
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
) -> backend.server.v2.library.model.LibraryAgentPreset:
try:
preset = await backend.server.v2.library.db.get_preset(user_id, preset_id)
if not preset:
raise fastapi.HTTPException(
status_code=404,
detail=f"Preset {preset_id} not found",
)
return preset
except Exception as e:
logger.exception(f"Exception occurred whilst getting preset: {e}")
raise fastapi.HTTPException(status_code=500, detail="Failed to get preset")
@router.post("/presets")
async def create_preset(
preset: backend.server.v2.library.model.CreateLibraryAgentPresetRequest,
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
) -> backend.server.v2.library.model.LibraryAgentPreset:
try:
return await backend.server.v2.library.db.create_or_update_preset(
user_id, preset
)
except Exception as e:
logger.exception(f"Exception occurred whilst creating preset: {e}")
raise fastapi.HTTPException(status_code=500, detail="Failed to create preset")
@router.put("/presets/{preset_id}")
async def update_preset(
preset_id: str,
preset: backend.server.v2.library.model.CreateLibraryAgentPresetRequest,
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
) -> backend.server.v2.library.model.LibraryAgentPreset:
try:
return await backend.server.v2.library.db.create_or_update_preset(
user_id, preset, preset_id
)
except Exception as e:
logger.exception(f"Exception occurred whilst updating preset: {e}")
raise fastapi.HTTPException(status_code=500, detail="Failed to update preset")
@router.delete("/presets/{preset_id}")
async def delete_preset(
preset_id: str,
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
):
try:
await backend.server.v2.library.db.delete_preset(user_id, preset_id)
return fastapi.Response(status_code=204)
except Exception as e:
logger.exception(f"Exception occurred whilst deleting preset: {e}")
raise fastapi.HTTPException(status_code=500, detail="Failed to delete preset")
@router.post(
path="/presets/{preset_id}/execute",
tags=["presets"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
)
async def execute_preset(
graph_id: str,
graph_version: int,
preset_id: str,
node_input: dict[typing.Any, typing.Any],
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
) -> dict[str, typing.Any]: # FIXME: add proper return type
try:
preset = await backend.server.v2.library.db.get_preset(user_id, preset_id)
if not preset:
raise fastapi.HTTPException(status_code=404, detail="Preset not found")
logger.info(f"Preset inputs: {preset.inputs}")
updated_node_input = node_input.copy()
# Merge in preset input values
for key, value in preset.inputs.items():
if key not in updated_node_input:
updated_node_input[key] = value
execution = execution_manager_client().add_execution(
graph_id=graph_id,
graph_version=graph_version,
data=updated_node_input,
user_id=user_id,
preset_id=preset_id,
)
logger.info(f"Execution added: {execution} with input: {updated_node_input}")
return {"id": execution.graph_exec_id}
except Exception as e:
msg = e.__str__().encode().decode("unicode_escape")
raise fastapi.HTTPException(status_code=400, detail=msg)

View File

@ -1,5 +1,3 @@
import datetime
import autogpt_libs.auth.depends import autogpt_libs.auth.depends
import autogpt_libs.auth.middleware import autogpt_libs.auth.middleware
import fastapi import fastapi
@ -27,9 +25,9 @@ def override_get_user_id():
return "test-user-id" return "test-user-id"
app.dependency_overrides[ app.dependency_overrides[autogpt_libs.auth.middleware.auth_middleware] = (
autogpt_libs.auth.middleware.auth_middleware override_auth_middleware
] = override_auth_middleware )
app.dependency_overrides[autogpt_libs.auth.depends.get_user_id] = override_get_user_id app.dependency_overrides[autogpt_libs.auth.depends.get_user_id] = override_get_user_id
@ -37,29 +35,21 @@ def test_get_library_agents_success(mocker: pytest_mock.MockFixture):
mocked_value = [ mocked_value = [
backend.server.v2.library.model.LibraryAgent( backend.server.v2.library.model.LibraryAgent(
id="test-agent-1", id="test-agent-1",
agent_id="test-agent-1", version=1,
agent_version=1, is_active=True,
preset_id="preset-1",
updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0),
is_favorite=False,
is_created_by_user=True,
is_latest_version=True,
name="Test Agent 1", name="Test Agent 1",
description="Test Description 1", description="Test Description 1",
isCreatedByUser=True,
input_schema={"type": "object", "properties": {}}, input_schema={"type": "object", "properties": {}},
output_schema={"type": "object", "properties": {}}, output_schema={"type": "object", "properties": {}},
), ),
backend.server.v2.library.model.LibraryAgent( backend.server.v2.library.model.LibraryAgent(
id="test-agent-2", id="test-agent-2",
agent_id="test-agent-2", version=1,
agent_version=1, is_active=True,
preset_id="preset-2",
updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0),
is_favorite=False,
is_created_by_user=False,
is_latest_version=True,
name="Test Agent 2", name="Test Agent 2",
description="Test Description 2", description="Test Description 2",
isCreatedByUser=False,
input_schema={"type": "object", "properties": {}}, input_schema={"type": "object", "properties": {}},
output_schema={"type": "object", "properties": {}}, output_schema={"type": "object", "properties": {}},
), ),
@ -75,10 +65,10 @@ def test_get_library_agents_success(mocker: pytest_mock.MockFixture):
for agent in response.json() for agent in response.json()
] ]
assert len(data) == 2 assert len(data) == 2
assert data[0].agent_id == "test-agent-1" assert data[0].id == "test-agent-1"
assert data[0].is_created_by_user is True assert data[0].isCreatedByUser is True
assert data[1].agent_id == "test-agent-2" assert data[1].id == "test-agent-2"
assert data[1].is_created_by_user is False assert data[1].isCreatedByUser is False
mock_db_call.assert_called_once_with("test-user-id") mock_db_call.assert_called_once_with("test-user-id")

View File

@ -325,10 +325,7 @@ async def get_store_submissions(
where = prisma.types.StoreSubmissionWhereInput(user_id=user_id) where = prisma.types.StoreSubmissionWhereInput(user_id=user_id)
# Query submissions from database # Query submissions from database
submissions = await prisma.models.StoreSubmission.prisma().find_many( submissions = await prisma.models.StoreSubmission.prisma().find_many(
where=where, where=where, skip=skip, take=page_size, order=[{"date_submitted": "desc"}]
skip=skip,
take=page_size,
order=[{"date_submitted": "desc"}],
) )
# Get total count for pagination # Get total count for pagination
@ -408,7 +405,9 @@ async def delete_store_submission(
) )
# Delete the submission # Delete the submission
await prisma.models.StoreListing.prisma().delete(where={"id": submission.id}) await prisma.models.StoreListing.prisma().delete(
where=prisma.types.StoreListingWhereUniqueInput(id=submission.id)
)
logger.debug( logger.debug(
f"Successfully deleted submission {submission_id} for user {user_id}" f"Successfully deleted submission {submission_id} for user {user_id}"
@ -505,15 +504,7 @@ async def create_store_submission(
"subHeading": sub_heading, "subHeading": sub_heading,
} }
}, },
}, }
include={"StoreListingVersions": True},
)
slv_id = (
listing.StoreListingVersions[0].id
if listing.StoreListingVersions is not None
and len(listing.StoreListingVersions) > 0
else None
) )
logger.debug(f"Created store listing for agent {agent_id}") logger.debug(f"Created store listing for agent {agent_id}")
@ -530,7 +521,6 @@ async def create_store_submission(
status=prisma.enums.SubmissionStatus.PENDING, status=prisma.enums.SubmissionStatus.PENDING,
runs=0, runs=0,
rating=0.0, rating=0.0,
store_listing_version_id=slv_id,
) )
except ( except (
@ -821,7 +811,9 @@ async def get_agent(
agent = store_listing_version.Agent agent = store_listing_version.Agent
graph = await backend.data.graph.get_graph(agent.id, agent.version) graph = await backend.data.graph.get_graph(
agent.id, agent.version, template=True
)
if not graph: if not graph:
raise fastapi.HTTPException( raise fastapi.HTTPException(
@ -840,74 +832,3 @@ async def get_agent(
raise backend.server.v2.store.exceptions.DatabaseError( raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch agent" "Failed to fetch agent"
) from e ) from e
async def review_store_submission(
store_listing_version_id: str, is_approved: bool, comments: str, reviewer_id: str
) -> prisma.models.StoreListingSubmission:
"""Review a store listing submission."""
try:
store_listing_version = (
await prisma.models.StoreListingVersion.prisma().find_unique(
where={"id": store_listing_version_id},
include={"StoreListing": True},
)
)
if not store_listing_version or not store_listing_version.StoreListing:
raise fastapi.HTTPException(
status_code=404,
detail=f"Store listing version {store_listing_version_id} not found",
)
status = (
prisma.enums.SubmissionStatus.APPROVED
if is_approved
else prisma.enums.SubmissionStatus.REJECTED
)
create_data = prisma.types.StoreListingSubmissionCreateInput(
StoreListingVersion={"connect": {"id": store_listing_version_id}},
Status=status,
reviewComments=comments,
Reviewer={"connect": {"id": reviewer_id}},
StoreListing={"connect": {"id": store_listing_version.StoreListing.id}},
createdAt=datetime.now(),
updatedAt=datetime.now(),
)
update_data = prisma.types.StoreListingSubmissionUpdateInput(
Status=status,
reviewComments=comments,
Reviewer={"connect": {"id": reviewer_id}},
StoreListing={"connect": {"id": store_listing_version.StoreListing.id}},
updatedAt=datetime.now(),
)
if is_approved:
await prisma.models.StoreListing.prisma().update(
where={"id": store_listing_version.StoreListing.id},
data={"isApproved": True},
)
submission = await prisma.models.StoreListingSubmission.prisma().upsert(
where={"storeListingVersionId": store_listing_version_id},
data=prisma.types.StoreListingSubmissionUpsertInput(
create=create_data,
update=update_data,
),
)
if not submission:
raise fastapi.HTTPException(
status_code=404,
detail=f"Store listing submission {store_listing_version_id} not found",
)
return submission
except Exception as e:
logger.error(f"Error reviewing store submission: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to review store submission"
) from e

View File

@ -115,7 +115,6 @@ class StoreSubmission(pydantic.BaseModel):
status: prisma.enums.SubmissionStatus status: prisma.enums.SubmissionStatus
runs: int runs: int
rating: float rating: float
store_listing_version_id: str | None = None
class StoreSubmissionsResponse(pydantic.BaseModel): class StoreSubmissionsResponse(pydantic.BaseModel):
@ -152,9 +151,3 @@ class StoreReviewCreate(pydantic.BaseModel):
store_listing_version_id: str store_listing_version_id: str
score: int score: int
comments: str | None = None comments: str | None = None
class ReviewSubmissionRequest(pydantic.BaseModel):
store_listing_version_id: str
isApproved: bool
comments: str

View File

@ -642,33 +642,3 @@ async def download_agent_file(
return fastapi.responses.FileResponse( return fastapi.responses.FileResponse(
tmp_file.name, filename=file_name, media_type="application/json" tmp_file.name, filename=file_name, media_type="application/json"
) )
@router.post(
"/submissions/review/{store_listing_version_id}",
tags=["store", "private"],
)
async def review_submission(
request: backend.server.v2.store.model.ReviewSubmissionRequest,
user: typing.Annotated[
autogpt_libs.auth.models.User,
fastapi.Depends(autogpt_libs.auth.depends.requires_admin_user),
],
):
# Proceed with the review submission logic
try:
submission = await backend.server.v2.store.db.review_store_submission(
store_listing_version_id=request.store_listing_version_id,
is_approved=request.isApproved,
comments=request.comments,
reviewer_id=user.user_id,
)
return submission
except Exception:
logger.exception("Exception occurred whilst reviewing store submission")
return fastapi.responses.JSONResponse(
status_code=500,
content={
"detail": "An error occurred while reviewing the store submission"
},
)

View File

@ -253,7 +253,7 @@ async def block_autogen_agent():
test_graph = await create_graph(create_test_graph(), user_id=test_user.id) test_graph = await create_graph(create_test_graph(), user_id=test_user.id)
input_data = {"input": "Write me a block that writes a string into a file."} input_data = {"input": "Write me a block that writes a string into a file."}
response = await server.agent_server.test_execute_graph( response = await server.agent_server.test_execute_graph(
test_graph.id, test_graph.version, input_data, test_user.id test_graph.id, input_data, test_user.id
) )
print(response) print(response)
result = await wait_execution( result = await wait_execution(

View File

@ -157,7 +157,7 @@ async def reddit_marketing_agent():
test_graph = await create_graph(create_test_graph(), user_id=test_user.id) test_graph = await create_graph(create_test_graph(), user_id=test_user.id)
input_data = {"subreddit": "AutoGPT"} input_data = {"subreddit": "AutoGPT"}
response = await server.agent_server.test_execute_graph( response = await server.agent_server.test_execute_graph(
test_graph.id, test_graph.version, input_data, test_user.id test_graph.id, input_data, test_user.id
) )
print(response) print(response)
result = await wait_execution(test_user.id, test_graph.id, response["id"], 120) result = await wait_execution(test_user.id, test_graph.id, response["id"], 120)

View File

@ -8,19 +8,12 @@ from backend.data.user import get_or_create_user
from backend.util.test import SpinTestServer, wait_execution from backend.util.test import SpinTestServer, wait_execution
async def create_test_user(alt_user: bool = False) -> User: async def create_test_user() -> User:
if alt_user: test_user_data = {
test_user_data = { "sub": "ef3b97d7-1161-4eb4-92b2-10c24fb154c1",
"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1b", "email": "testuser#example.com",
"email": "testuser2#example.com", "name": "Test User",
"name": "Test User 2", }
}
else:
test_user_data = {
"sub": "ef3b97d7-1161-4eb4-92b2-10c24fb154c1",
"email": "testuser#example.com",
"name": "Test User",
}
user = await get_or_create_user(test_user_data) user = await get_or_create_user(test_user_data)
return user return user
@ -86,7 +79,7 @@ async def sample_agent():
test_graph = await create_graph(create_test_graph(), test_user.id) test_graph = await create_graph(create_test_graph(), test_user.id)
input_data = {"input_1": "Hello", "input_2": "World"} input_data = {"input_1": "Hello", "input_2": "World"}
response = await server.agent_server.test_execute_graph( response = await server.agent_server.test_execute_graph(
test_graph.id, test_graph.version, input_data, test_user.id test_graph.id, input_data, test_user.id
) )
print(response) print(response)
result = await wait_execution(test_user.id, test_graph.id, response["id"], 10) result = await wait_execution(test_user.id, test_graph.id, response["id"], 10)

View File

@ -73,10 +73,9 @@ async def wait_execution(
# Wait for the executions to complete # Wait for the executions to complete
for i in range(timeout): for i in range(timeout):
if await is_execution_completed(): if await is_execution_completed():
graph_exec = await AgentServer().test_get_graph_run_results( return await AgentServer().test_get_graph_run_node_execution_results(
graph_id, graph_exec_id, user_id graph_id, graph_exec_id, user_id
) )
return graph_exec.node_executions
time.sleep(1) time.sleep(1)
assert False, "Execution did not complete in time." assert False, "Execution did not complete in time."

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "AgentPreset" ADD COLUMN "isDeleted" BOOLEAN NOT NULL DEFAULT false;

View File

@ -1,46 +0,0 @@
/*
Warnings:
- You are about to drop the `UserAgent` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "UserAgent" DROP CONSTRAINT "UserAgent_agentId_agentVersion_fkey";
-- DropForeignKey
ALTER TABLE "UserAgent" DROP CONSTRAINT "UserAgent_agentPresetId_fkey";
-- DropForeignKey
ALTER TABLE "UserAgent" DROP CONSTRAINT "UserAgent_userId_fkey";
-- DropTable
DROP TABLE "UserAgent";
-- CreateTable
CREATE TABLE "LibraryAgent" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" TEXT NOT NULL,
"agentId" TEXT NOT NULL,
"agentVersion" INTEGER NOT NULL,
"agentPresetId" TEXT,
"isFavorite" BOOLEAN NOT NULL DEFAULT false,
"isCreatedByUser" BOOLEAN NOT NULL DEFAULT false,
"isArchived" BOOLEAN NOT NULL DEFAULT false,
"isDeleted" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "LibraryAgent_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "LibraryAgent_userId_idx" ON "LibraryAgent"("userId");
-- AddForeignKey
ALTER TABLE "LibraryAgent" ADD CONSTRAINT "LibraryAgent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LibraryAgent" ADD CONSTRAINT "LibraryAgent_agentId_agentVersion_fkey" FOREIGN KEY ("agentId", "agentVersion") REFERENCES "AgentGraph"("id", "version") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LibraryAgent" ADD CONSTRAINT "LibraryAgent_agentPresetId_fkey" FOREIGN KEY ("agentPresetId") REFERENCES "AgentPreset"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "LibraryAgent" ADD COLUMN "useGraphIsActiveVersion" BOOLEAN NOT NULL DEFAULT false;

View File

@ -1,29 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[agentId]` on the table `StoreListing` will be added. If there are existing duplicate values, this will fail.
*/
-- DropIndex
DROP INDEX "StoreListing_agentId_idx";
-- DropIndex
DROP INDEX "StoreListing_isApproved_idx";
-- DropIndex
DROP INDEX "StoreListingVersion_agentId_agentVersion_isApproved_idx";
-- CreateIndex
CREATE INDEX "StoreListing_agentId_owningUserId_idx" ON "StoreListing"("agentId", "owningUserId");
-- CreateIndex
CREATE INDEX "StoreListing_isDeleted_isApproved_idx" ON "StoreListing"("isDeleted", "isApproved");
-- CreateIndex
CREATE INDEX "StoreListing_isDeleted_idx" ON "StoreListing"("isDeleted");
-- CreateIndex
CREATE UNIQUE INDEX "StoreListing_agentId_key" ON "StoreListing"("agentId");
-- CreateIndex
CREATE INDEX "StoreListingVersion_agentId_agentVersion_isDeleted_idx" ON "StoreListingVersion"("agentId", "agentVersion", "isDeleted");

View File

@ -30,7 +30,7 @@ model User {
CreditTransaction CreditTransaction[] CreditTransaction CreditTransaction[]
AgentPreset AgentPreset[] AgentPreset AgentPreset[]
LibraryAgent LibraryAgent[] UserAgent UserAgent[]
Profile Profile[] Profile Profile[]
StoreListing StoreListing[] StoreListing StoreListing[]
@ -65,7 +65,7 @@ model AgentGraph {
AgentGraphExecution AgentGraphExecution[] AgentGraphExecution AgentGraphExecution[]
AgentPreset AgentPreset[] AgentPreset AgentPreset[]
LibraryAgent LibraryAgent[] UserAgent UserAgent[]
StoreListing StoreListing[] StoreListing StoreListing[]
StoreListingVersion StoreListingVersion? StoreListingVersion StoreListingVersion?
@ -103,17 +103,15 @@ model AgentPreset {
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade) Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
InputPresets AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData") InputPresets AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData")
LibraryAgents LibraryAgent[] UserAgents UserAgent[]
AgentExecution AgentGraphExecution[] AgentExecution AgentGraphExecution[]
isDeleted Boolean @default(false)
@@index([userId]) @@index([userId])
} }
// For the library page // For the library page
// It is a user controlled list of agents, that they will see in there library // It is a user controlled list of agents, that they will see in there library
model LibraryAgent { model UserAgent {
id String @id @default(uuid()) id String @id @default(uuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
@ -128,8 +126,6 @@ model LibraryAgent {
agentPresetId String? agentPresetId String?
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id]) AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
useGraphIsActiveVersion Boolean @default(false)
isFavorite Boolean @default(false) isFavorite Boolean @default(false)
isCreatedByUser Boolean @default(false) isCreatedByUser Boolean @default(false)
isArchived Boolean @default(false) isArchived Boolean @default(false)
@ -239,7 +235,7 @@ model AgentGraphExecution {
AgentNodeExecutions AgentNodeExecution[] AgentNodeExecutions AgentNodeExecution[]
// Link to User model -- Executed by this user // Link to User model
userId String userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@ -447,8 +443,6 @@ view Creator {
agent_rating Float agent_rating Float
agent_runs Int agent_runs Int
is_featured Boolean is_featured Boolean
// Index or unique are not applied to views
} }
view StoreAgent { view StoreAgent {
@ -471,7 +465,11 @@ view StoreAgent {
rating Float rating Float
versions String[] versions String[]
// Index or unique are not applied to views @@unique([creator_username, slug])
@@index([creator_username])
@@index([featured])
@@index([categories])
@@index([storeListingVersionId])
} }
view StoreSubmission { view StoreSubmission {
@ -489,7 +487,7 @@ view StoreSubmission {
agent_id String agent_id String
agent_version Int agent_version Int
// Index or unique are not applied to views @@index([user_id])
} }
model StoreListing { model StoreListing {
@ -512,13 +510,9 @@ model StoreListing {
StoreListingVersions StoreListingVersion[] StoreListingVersions StoreListingVersion[]
StoreListingSubmission StoreListingSubmission[] StoreListingSubmission StoreListingSubmission[]
// Unique index on agentId to ensure only one listing per agent, regardless of number of versions the agent has. @@index([isApproved])
@@unique([agentId]) @@index([agentId])
@@index([agentId, owningUserId])
@@index([owningUserId]) @@index([owningUserId])
// Used in the view query
@@index([isDeleted, isApproved])
@@index([isDeleted])
} }
model StoreListingVersion { model StoreListingVersion {
@ -559,7 +553,7 @@ model StoreListingVersion {
StoreListingReview StoreListingReview[] StoreListingReview StoreListingReview[]
@@unique([agentId, agentVersion]) @@unique([agentId, agentVersion])
@@index([agentId, agentVersion, isDeleted]) @@index([agentId, agentVersion, isApproved])
} }
model StoreListingReview { model StoreListingReview {

View File

@ -2,22 +2,22 @@ import logging
import pytest import pytest
from backend.util.logging import configure_logging from backend.util.test import SpinTestServer
# NOTE: You can run tests like with the --log-cli-level=INFO to see the logs # NOTE: You can run tests like with the --log-cli-level=INFO to see the logs
# Set up logging # Set up logging
configure_logging()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Reduce Prisma log spam # Create console handler with formatting
prisma_logger = logging.getLogger("prisma") ch = logging.StreamHandler()
prisma_logger.setLevel(logging.INFO) ch.setLevel(logging.INFO)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
ch.setFormatter(formatter)
logger.addHandler(ch)
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
async def server(): async def server():
from backend.util.test import SpinTestServer
async with SpinTestServer() as server: async with SpinTestServer() as server:
yield server yield server

View File

@ -1,11 +1,8 @@
from typing import Any from typing import Any
from uuid import UUID from uuid import UUID
import autogpt_libs.auth.models
import fastapi.exceptions
import pytest import pytest
import backend.server.v2.store.model
from backend.blocks.basic import AgentInputBlock, AgentOutputBlock, StoreValueBlock from backend.blocks.basic import AgentInputBlock, AgentOutputBlock, StoreValueBlock
from backend.data.block import BlockSchema from backend.data.block import BlockSchema
from backend.data.graph import Graph, Link, Node from backend.data.graph import Graph, Link, Node
@ -205,92 +202,3 @@ async def test_clean_graph(server: SpinTestServer):
n for n in created_graph.nodes if n.block_id == AgentInputBlock().id n for n in created_graph.nodes if n.block_id == AgentInputBlock().id
) )
assert input_node.input_default["value"] == "" assert input_node.input_default["value"] == ""
@pytest.mark.asyncio(scope="session")
async def test_access_store_listing_graph(server: SpinTestServer):
"""
Test the access of a store listing graph.
"""
graph = Graph(
id="test_clean_graph",
name="Test Clean Graph",
description="Test graph cleaning",
nodes=[
Node(
id="input_node",
block_id=AgentInputBlock().id,
input_default={
"name": "test_input",
"value": "test value",
"description": "Test input description",
},
),
],
links=[],
)
# Create graph and get model
create_graph = CreateGraph(graph=graph)
created_graph = await server.agent_server.test_create_graph(
create_graph, DEFAULT_USER_ID
)
store_submission_request = backend.server.v2.store.model.StoreSubmissionRequest(
agent_id=created_graph.id,
agent_version=created_graph.version,
slug="test-slug",
name="Test name",
sub_heading="Test sub heading",
video_url=None,
image_urls=[],
description="Test description",
categories=[],
)
# First we check the graph an not be accessed by a different user
with pytest.raises(fastapi.exceptions.HTTPException) as exc_info:
await server.agent_server.test_get_graph(
created_graph.id,
created_graph.version,
"3e53486c-cf57-477e-ba2a-cb02dc828e1b",
)
assert exc_info.value.status_code == 404
assert "Graph" in str(exc_info.value.detail)
# Now we create a store listing
store_listing = await server.agent_server.test_create_store_listing(
store_submission_request, DEFAULT_USER_ID
)
if isinstance(store_listing, fastapi.responses.JSONResponse):
assert False, "Failed to create store listing"
slv_id = (
store_listing.store_listing_version_id
if store_listing.store_listing_version_id is not None
else None
)
assert slv_id is not None
admin = autogpt_libs.auth.models.User(
user_id="3e53486c-cf57-477e-ba2a-cb02dc828e1b",
role="admin",
email="admin@example.com",
phone_number="1234567890",
)
await server.agent_server.test_review_store_listing(
backend.server.v2.store.model.ReviewSubmissionRequest(
store_listing_version_id=slv_id,
isApproved=True,
comments="Test comments",
),
admin,
)
# Now we check the graph can be accessed by a user that does not own the graph
got_graph = await server.agent_server.test_get_graph(
created_graph.id, created_graph.version, "3e53486c-cf57-477e-ba2a-cb02dc828e1b"
)
assert got_graph is not None

View File

@ -1,13 +1,9 @@
import logging import logging
import autogpt_libs.auth.models
import fastapi.responses
import pytest import pytest
from prisma.models import User from prisma.models import User
import backend.server.v2.library.model from backend.blocks.basic import FindInDictionaryBlock, StoreValueBlock
import backend.server.v2.store.model
from backend.blocks.basic import AgentInputBlock, FindInDictionaryBlock, StoreValueBlock
from backend.blocks.maths import CalculatorBlock, Operation from backend.blocks.maths import CalculatorBlock, Operation
from backend.data import execution, graph from backend.data import execution, graph
from backend.server.model import CreateGraph from backend.server.model import CreateGraph
@ -35,7 +31,7 @@ async def execute_graph(
# --- Test adding new executions --- # # --- Test adding new executions --- #
response = await agent_server.test_execute_graph( response = await agent_server.test_execute_graph(
test_graph.id, test_graph.version, input_data, test_user.id test_graph.id, input_data, test_user.id
) )
graph_exec_id = response["id"] graph_exec_id = response["id"]
logger.info(f"Created execution with ID: {graph_exec_id}") logger.info(f"Created execution with ID: {graph_exec_id}")
@ -55,7 +51,7 @@ async def assert_sample_graph_executions(
graph_exec_id: str, graph_exec_id: str,
): ):
logger.info(f"Checking execution results for graph {test_graph.id}") logger.info(f"Checking execution results for graph {test_graph.id}")
graph_run = await agent_server.test_get_graph_run_results( executions = await agent_server.test_get_graph_run_node_execution_results(
test_graph.id, test_graph.id,
graph_exec_id, graph_exec_id,
test_user.id, test_user.id,
@ -74,7 +70,7 @@ async def assert_sample_graph_executions(
] ]
# Executing StoreValueBlock # Executing StoreValueBlock
exec = graph_run.node_executions[0] exec = executions[0]
logger.info(f"Checking first StoreValueBlock execution: {exec}") logger.info(f"Checking first StoreValueBlock execution: {exec}")
assert exec.status == execution.ExecutionStatus.COMPLETED assert exec.status == execution.ExecutionStatus.COMPLETED
assert exec.graph_exec_id == graph_exec_id assert exec.graph_exec_id == graph_exec_id
@ -87,7 +83,7 @@ async def assert_sample_graph_executions(
assert exec.node_id in [test_graph.nodes[0].id, test_graph.nodes[1].id] assert exec.node_id in [test_graph.nodes[0].id, test_graph.nodes[1].id]
# Executing StoreValueBlock # Executing StoreValueBlock
exec = graph_run.node_executions[1] exec = executions[1]
logger.info(f"Checking second StoreValueBlock execution: {exec}") logger.info(f"Checking second StoreValueBlock execution: {exec}")
assert exec.status == execution.ExecutionStatus.COMPLETED assert exec.status == execution.ExecutionStatus.COMPLETED
assert exec.graph_exec_id == graph_exec_id assert exec.graph_exec_id == graph_exec_id
@ -100,7 +96,7 @@ async def assert_sample_graph_executions(
assert exec.node_id in [test_graph.nodes[0].id, test_graph.nodes[1].id] assert exec.node_id in [test_graph.nodes[0].id, test_graph.nodes[1].id]
# Executing FillTextTemplateBlock # Executing FillTextTemplateBlock
exec = graph_run.node_executions[2] exec = executions[2]
logger.info(f"Checking FillTextTemplateBlock execution: {exec}") logger.info(f"Checking FillTextTemplateBlock execution: {exec}")
assert exec.status == execution.ExecutionStatus.COMPLETED assert exec.status == execution.ExecutionStatus.COMPLETED
assert exec.graph_exec_id == graph_exec_id assert exec.graph_exec_id == graph_exec_id
@ -115,7 +111,7 @@ async def assert_sample_graph_executions(
assert exec.node_id == test_graph.nodes[2].id assert exec.node_id == test_graph.nodes[2].id
# Executing PrintToConsoleBlock # Executing PrintToConsoleBlock
exec = graph_run.node_executions[3] exec = executions[3]
logger.info(f"Checking PrintToConsoleBlock execution: {exec}") logger.info(f"Checking PrintToConsoleBlock execution: {exec}")
assert exec.status == execution.ExecutionStatus.COMPLETED assert exec.status == execution.ExecutionStatus.COMPLETED
assert exec.graph_exec_id == graph_exec_id assert exec.graph_exec_id == graph_exec_id
@ -198,14 +194,14 @@ async def test_input_pin_always_waited(server: SpinTestServer):
) )
logger.info("Checking execution results") logger.info("Checking execution results")
graph_exec = await server.agent_server.test_get_graph_run_results( executions = await server.agent_server.test_get_graph_run_node_execution_results(
test_graph.id, graph_exec_id, test_user.id test_graph.id, graph_exec_id, test_user.id
) )
assert len(graph_exec.node_executions) == 3 assert len(executions) == 3
# FindInDictionaryBlock should wait for the input pin to be provided, # FindInDictionaryBlock should wait for the input pin to be provided,
# Hence executing extraction of "key" from {"key1": "value1", "key2": "value2"} # Hence executing extraction of "key" from {"key1": "value1", "key2": "value2"}
assert graph_exec.node_executions[2].status == execution.ExecutionStatus.COMPLETED assert executions[2].status == execution.ExecutionStatus.COMPLETED
assert graph_exec.node_executions[2].output_data == {"output": ["value2"]} assert executions[2].output_data == {"output": ["value2"]}
logger.info("Completed test_input_pin_always_waited") logger.info("Completed test_input_pin_always_waited")
@ -281,265 +277,13 @@ async def test_static_input_link_on_graph(server: SpinTestServer):
server.agent_server, test_graph, test_user, {}, 8 server.agent_server, test_graph, test_user, {}, 8
) )
logger.info("Checking execution results") logger.info("Checking execution results")
graph_exec = await server.agent_server.test_get_graph_run_results( executions = await server.agent_server.test_get_graph_run_node_execution_results(
test_graph.id, graph_exec_id, test_user.id test_graph.id, graph_exec_id, test_user.id
) )
assert len(graph_exec.node_executions) == 8 assert len(executions) == 8
# The last 3 executions will be a+b=4+5=9 # The last 3 executions will be a+b=4+5=9
for i, exec_data in enumerate(graph_exec.node_executions[-3:]): for i, exec_data in enumerate(executions[-3:]):
logger.info(f"Checking execution {i+1} of last 3: {exec_data}") logger.info(f"Checking execution {i+1} of last 3: {exec_data}")
assert exec_data.status == execution.ExecutionStatus.COMPLETED assert exec_data.status == execution.ExecutionStatus.COMPLETED
assert exec_data.output_data == {"result": [9]} assert exec_data.output_data == {"result": [9]}
logger.info("Completed test_static_input_link_on_graph") logger.info("Completed test_static_input_link_on_graph")
@pytest.mark.asyncio(scope="session")
async def test_execute_preset(server: SpinTestServer):
"""
Test executing a preset.
This test ensures that:
1. A preset can be successfully executed
2. The execution results are correct
Args:
server (SpinTestServer): The test server instance.
"""
# Create test graph and user
nodes = [
graph.Node( # 0
block_id=AgentInputBlock().id,
input_default={"name": "dictionary"},
),
graph.Node( # 1
block_id=AgentInputBlock().id,
input_default={"name": "selected_value"},
),
graph.Node( # 2
block_id=StoreValueBlock().id,
input_default={"input": {"key1": "Hi", "key2": "Everyone"}},
),
graph.Node( # 3
block_id=FindInDictionaryBlock().id,
input_default={"key": "", "input": {}},
),
]
links = [
graph.Link(
source_id=nodes[0].id,
sink_id=nodes[2].id,
source_name="result",
sink_name="input",
),
graph.Link(
source_id=nodes[1].id,
sink_id=nodes[3].id,
source_name="result",
sink_name="key",
),
graph.Link(
source_id=nodes[2].id,
sink_id=nodes[3].id,
source_name="output",
sink_name="input",
),
]
test_graph = graph.Graph(
name="TestGraph",
description="Test graph",
nodes=nodes,
links=links,
)
test_user = await create_test_user()
test_graph = await create_graph(server, test_graph, test_user)
# Create preset with initial values
preset = backend.server.v2.library.model.CreateLibraryAgentPresetRequest(
name="Test Preset With Clash",
description="Test preset with clashing input values",
agent_id=test_graph.id,
agent_version=test_graph.version,
inputs={
"dictionary": {"key1": "Hello", "key2": "World"},
"selected_value": "key2",
},
is_active=True,
)
created_preset = await server.agent_server.test_create_preset(preset, test_user.id)
# Execute preset with overriding values
result = await server.agent_server.test_execute_preset(
graph_id=test_graph.id,
graph_version=test_graph.version,
preset_id=created_preset.id,
node_input={},
user_id=test_user.id,
)
# Verify execution
assert result is not None
graph_exec_id = result["id"]
# Wait for execution to complete
executions = await wait_execution(test_user.id, test_graph.id, graph_exec_id)
assert len(executions) == 4
# FindInDictionaryBlock should wait for the input pin to be provided,
# Hence executing extraction of "key" from {"key1": "value1", "key2": "value2"}
assert executions[3].status == execution.ExecutionStatus.COMPLETED
assert executions[3].output_data == {"output": ["World"]}
@pytest.mark.asyncio(scope="session")
async def test_execute_preset_with_clash(server: SpinTestServer):
"""
Test executing a preset with clashing input data.
"""
# Create test graph and user
nodes = [
graph.Node( # 0
block_id=AgentInputBlock().id,
input_default={"name": "dictionary"},
),
graph.Node( # 1
block_id=AgentInputBlock().id,
input_default={"name": "selected_value"},
),
graph.Node( # 2
block_id=StoreValueBlock().id,
input_default={"input": {"key1": "Hi", "key2": "Everyone"}},
),
graph.Node( # 3
block_id=FindInDictionaryBlock().id,
input_default={"key": "", "input": {}},
),
]
links = [
graph.Link(
source_id=nodes[0].id,
sink_id=nodes[2].id,
source_name="result",
sink_name="input",
),
graph.Link(
source_id=nodes[1].id,
sink_id=nodes[3].id,
source_name="result",
sink_name="key",
),
graph.Link(
source_id=nodes[2].id,
sink_id=nodes[3].id,
source_name="output",
sink_name="input",
),
]
test_graph = graph.Graph(
name="TestGraph",
description="Test graph",
nodes=nodes,
links=links,
)
test_user = await create_test_user()
test_graph = await create_graph(server, test_graph, test_user)
# Create preset with initial values
preset = backend.server.v2.library.model.CreateLibraryAgentPresetRequest(
name="Test Preset With Clash",
description="Test preset with clashing input values",
agent_id=test_graph.id,
agent_version=test_graph.version,
inputs={
"dictionary": {"key1": "Hello", "key2": "World"},
"selected_value": "key2",
},
is_active=True,
)
created_preset = await server.agent_server.test_create_preset(preset, test_user.id)
# Execute preset with overriding values
result = await server.agent_server.test_execute_preset(
graph_id=test_graph.id,
graph_version=test_graph.version,
preset_id=created_preset.id,
node_input={"selected_value": "key1"},
user_id=test_user.id,
)
# Verify execution
assert result is not None
graph_exec_id = result["id"]
# Wait for execution to complete
executions = await wait_execution(test_user.id, test_graph.id, graph_exec_id)
assert len(executions) == 4
# FindInDictionaryBlock should wait for the input pin to be provided,
# Hence executing extraction of "key" from {"key1": "value1", "key2": "value2"}
assert executions[3].status == execution.ExecutionStatus.COMPLETED
assert executions[3].output_data == {"output": ["Hello"]}
@pytest.mark.asyncio(scope="session")
async def test_store_listing_graph(server: SpinTestServer):
logger.info("Starting test_agent_execution")
test_user = await create_test_user()
test_graph = await create_graph(server, create_test_graph(), test_user)
store_submission_request = backend.server.v2.store.model.StoreSubmissionRequest(
agent_id=test_graph.id,
agent_version=test_graph.version,
slug="test-slug",
name="Test name",
sub_heading="Test sub heading",
video_url=None,
image_urls=[],
description="Test description",
categories=[],
)
store_listing = await server.agent_server.test_create_store_listing(
store_submission_request, test_user.id
)
if isinstance(store_listing, fastapi.responses.JSONResponse):
assert False, "Failed to create store listing"
slv_id = (
store_listing.store_listing_version_id
if store_listing.store_listing_version_id is not None
else None
)
assert slv_id is not None
admin = autogpt_libs.auth.models.User(
user_id="3e53486c-cf57-477e-ba2a-cb02dc828e1b",
role="admin",
email="admin@example.com",
phone_number="1234567890",
)
await server.agent_server.test_review_store_listing(
backend.server.v2.store.model.ReviewSubmissionRequest(
store_listing_version_id=slv_id,
isApproved=True,
comments="Test comments",
),
admin,
)
alt_test_user = await create_test_user(alt_user=True)
data = {"input_1": "Hello", "input_2": "World"}
graph_exec_id = await execute_graph(
server.agent_server,
test_graph,
alt_test_user,
data,
4,
)
await assert_sample_graph_executions(
server.agent_server, test_graph, alt_test_user, graph_exec_id
)
logger.info("Completed test_agent_execution")

View File

@ -140,10 +140,10 @@ async def main():
print(f"Inserting {NUM_USERS * MAX_AGENTS_PER_USER} user agents") print(f"Inserting {NUM_USERS * MAX_AGENTS_PER_USER} user agents")
for user in users: for user in users:
num_agents = random.randint(MIN_AGENTS_PER_USER, MAX_AGENTS_PER_USER) num_agents = random.randint(MIN_AGENTS_PER_USER, MAX_AGENTS_PER_USER)
for _ in range(num_agents): # Create 1 LibraryAgent per user for _ in range(num_agents): # Create 1 UserAgent per user
graph = random.choice(agent_graphs) graph = random.choice(agent_graphs)
preset = random.choice(agent_presets) preset = random.choice(agent_presets)
user_agent = await db.libraryagent.create( user_agent = await db.useragent.create(
data={ data={
"userId": user.id, "userId": user.id,
"agentId": graph.id, "agentId": graph.id,

View File

@ -35,7 +35,6 @@
"@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.1.4", "@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-radio-group": "^1.2.1", "@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.1", "@radix-ui/react-scroll-area": "^1.2.1",
@ -43,7 +42,6 @@
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.4", "@radix-ui/react-toast": "^1.2.4",
"@radix-ui/react-tooltip": "^1.1.6", "@radix-ui/react-tooltip": "^1.1.6",
"@sentry/nextjs": "^8", "@sentry/nextjs": "^8",

View File

@ -1,668 +0,0 @@
"use client";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { Plus } from "lucide-react";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import {
GraphExecution,
GraphExecutionMeta,
Schedule,
GraphMeta,
BlockIOSubType,
} from "@/lib/autogpt-server-api";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button, ButtonProps } from "@/components/agptui/Button";
import { AgentRunStatus } from "@/components/agptui/AgentRunStatusChip";
import AgentRunSummaryCard from "@/components/agptui/AgentRunSummaryCard";
import moment from "moment";
const agentRunStatusMap: Record<GraphExecutionMeta["status"], AgentRunStatus> =
{
COMPLETED: "success",
FAILED: "failed",
QUEUED: "queued",
RUNNING: "running",
TERMINATED: "stopped",
// TODO: implement "draft" - https://github.com/Significant-Gravitas/AutoGPT/issues/9168
};
export default function AgentRunsPage(): React.ReactElement {
const { id: agentID }: { id: string } = useParams();
const router = useRouter();
const api = useBackendAPI();
const [agent, setAgent] = useState<GraphMeta | null>(null);
const [agentRuns, setAgentRuns] = useState<GraphExecutionMeta[]>([]);
const [schedules, setSchedules] = useState<Schedule[]>([]);
const [selectedView, selectView] = useState<{
type: "run" | "schedule";
id?: string;
}>({ type: "run" });
const [selectedRun, setSelectedRun] = useState<
GraphExecution | GraphExecutionMeta | null
>(null);
const [selectedSchedule, setSelectedSchedule] = useState<Schedule | null>(
null,
);
const [activeListTab, setActiveListTab] = useState<"runs" | "scheduled">(
"runs",
);
const [isFirstLoad, setIsFirstLoad] = useState<boolean>(true);
const openRunDraftView = useCallback(() => {
selectView({ type: "run" });
}, []);
const selectRun = useCallback((id: string) => {
selectView({ type: "run", id });
}, []);
const selectSchedule = useCallback((schedule: Schedule) => {
selectView({ type: "schedule", id: schedule.id });
setSelectedSchedule(schedule);
}, []);
const fetchAgents = useCallback(() => {
api.getGraph(agentID).then(setAgent);
api.getGraphExecutions(agentID).then((agentRuns) => {
const sortedRuns = agentRuns.toSorted(
(a, b) => b.started_at - a.started_at,
);
setAgentRuns(sortedRuns);
if (!selectedView.id && isFirstLoad && sortedRuns.length > 0) {
// only for first load or first execution
setIsFirstLoad(false);
selectView({ type: "run", id: sortedRuns[0].execution_id });
setSelectedRun(sortedRuns[0]);
}
});
if (selectedView.type == "run" && selectedView.id) {
api.getGraphExecutionInfo(agentID, selectedView.id).then(setSelectedRun);
}
}, [api, agentID, selectedView, isFirstLoad]);
useEffect(() => {
fetchAgents();
}, []);
// load selectedRun based on selectedView
useEffect(() => {
if (selectedView.type != "run" || !selectedView.id) return;
// pull partial data from "cache" while waiting for the rest to load
if (selectedView.id !== selectedRun?.execution_id) {
setSelectedRun(
agentRuns.find((r) => r.execution_id == selectedView.id) ?? null,
);
}
api.getGraphExecutionInfo(agentID, selectedView.id).then(setSelectedRun);
}, [api, selectedView, agentRuns, agentID]);
const fetchSchedules = useCallback(async () => {
// TODO: filter in backend - https://github.com/Significant-Gravitas/AutoGPT/issues/9183
setSchedules(
(await api.listSchedules()).filter((s) => s.graph_id == agentID),
);
}, [api, agentID]);
useEffect(() => {
fetchSchedules();
}, [fetchSchedules]);
const removeSchedule = useCallback(
async (scheduleId: string) => {
const removedSchedule = await api.deleteSchedule(scheduleId);
setSchedules(schedules.filter((s) => s.id !== removedSchedule.id));
},
[schedules, api],
);
/* TODO: use websockets instead of polling - https://github.com/Significant-Gravitas/AutoGPT/issues/8782 */
useEffect(() => {
const intervalId = setInterval(() => fetchAgents(), 5000);
return () => clearInterval(intervalId);
}, [fetchAgents, agent]);
const agentActions: { label: string; callback: () => void }[] = useMemo(
() => [
{
label: "Open in builder",
callback: () => agent && router.push(`/build?flowID=${agent.id}`),
},
],
[agent, router],
);
if (!agent) {
/* TODO: implement loading indicators / skeleton page */
return <span>Loading...</span>;
}
return (
<div className="container justify-stretch p-0 lg:flex">
{/* Sidebar w/ list of runs */}
{/* TODO: separate this out as a component */}
{/* TODO: render this below header in sm and md layouts */}
<aside className="agpt-div flex w-full flex-col gap-4 border-b lg:w-auto lg:border-b-0 lg:border-r">
<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={() => openRunDraftView()}
>
<Plus className="h-6 w-6" />
<span>New run</span>
</Button>
{/* Runs / Scheduled list switcher */}
<div className="flex gap-2">
<Badge
variant={activeListTab === "runs" ? "secondary" : "outline"}
className="cursor-pointer gap-2 rounded-full text-base"
onClick={() => setActiveListTab("runs")}
>
<span>Runs</span>
<span className="text-neutral-600">{agentRuns.length}</span>
</Badge>
<Badge
variant={activeListTab === "scheduled" ? "secondary" : "outline"}
className="cursor-pointer gap-2 rounded-full text-base"
onClick={() => setActiveListTab("scheduled")}
>
<span>Scheduled</span>
<span className="text-neutral-600">
{schedules.filter((s) => s.graph_id === agentID).length}
</span>
</Badge>
</div>
{/* Runs / Schedules list */}
<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={() => openRunDraftView()}
>
<Plus className="h-6 w-6" />
<span>New run</span>
</Button>
{activeListTab === "runs"
? agentRuns.map((run, i) => (
<AgentRunSummaryCard
className="h-28 w-72 lg:h-32 xl:w-80"
key={i}
agentID={run.graph_id}
agentRunID={run.execution_id}
status={agentRunStatusMap[run.status]}
title={agent.name}
timestamp={run.started_at}
selected={selectedView.id === run.execution_id}
onClick={() => selectRun(run.execution_id)}
/>
))
: schedules
.filter((schedule) => schedule.graph_id === agentID)
.map((schedule, i) => (
<AgentRunSummaryCard
className="h-28 w-72 lg:h-32 xl:w-80"
key={i}
agentID={schedule.graph_id}
agentRunID={schedule.id}
status="scheduled"
title={schedule.name}
timestamp={schedule.next_run_time} // FIXME
selected={selectedView.id === schedule.id}
onClick={() => selectSchedule(schedule)}
/>
))}
</div>
</ScrollArea>
</aside>
<div className="flex-1">
{/* Header */}
<div className="agpt-div w-full border-b">
<h1 className="font-poppins text-3xl font-medium">
{
agent.name /* TODO: use dynamic/custom run title - https://github.com/Significant-Gravitas/AutoGPT/issues/9184 */
}
</h1>
</div>
{/* Run / Schedule views */}
{(selectedView.type == "run" ? (
selectedView.id ? (
selectedRun && (
<AgentRunDetailsView
agent={agent}
run={selectedRun}
agentActions={agentActions}
/>
)
) : (
<AgentRunDraftView
agent={agent}
onRun={(runID) => selectRun(runID)}
agentActions={agentActions}
/>
)
) : selectedView.type == "schedule" ? (
selectedSchedule && (
<AgentScheduleDetailsView
agent={agent}
schedule={selectedSchedule}
onForcedRun={(runID) => selectRun(runID)}
agentActions={agentActions}
/>
)
) : null) || <p>Loading...</p>}
</div>
</div>
);
}
function AgentRunDetailsView({
agent,
run,
agentActions,
}: {
agent: GraphMeta;
run: GraphExecution | GraphExecutionMeta;
agentActions: { label: string; callback: () => void }[];
}): React.ReactNode {
const api = useBackendAPI();
const selectedRunStatus: AgentRunStatus = useMemo(
() => agentRunStatusMap[run.status],
[run],
);
const infoStats: { label: string; value: React.ReactNode }[] = useMemo(() => {
if (!run) return [];
return [
{
label: "Status",
value:
selectedRunStatus.charAt(0).toUpperCase() +
selectedRunStatus.slice(1),
},
{
label: "Started",
value: `${moment(run.started_at).fromNow()}, ${moment(run.started_at).format("HH:mm")}`,
},
{
label: "Duration",
value: `${moment.duration(run.duration, "seconds").humanize()}`,
},
// { label: "Cost", value: selectedRun.cost }, // TODO: implement cost - https://github.com/Significant-Gravitas/AutoGPT/issues/9181
];
}, [run, selectedRunStatus]);
const agentRunInputs:
| Record<string, { type: BlockIOSubType; value: any }>
| undefined = useMemo(() => {
if (!("inputs" in run)) return undefined;
// TODO: show (link to) preset - https://github.com/Significant-Gravitas/AutoGPT/issues/9168
// Add type info from agent input schema
return Object.fromEntries(
Object.entries(run.inputs).map(([k, v]) => [
k,
{ value: v, type: agent.input_schema.properties[k].type },
]),
);
}, [agent, run]);
const runAgain = useCallback(
() =>
agentRunInputs &&
api.executeGraph(
agent.id,
Object.fromEntries(
Object.entries(agentRunInputs).map(([k, v]) => [k, v.value]),
),
),
[api, agent, agentRunInputs],
);
const agentRunOutputs:
| Record<string, { type: BlockIOSubType; value: any }>
| null
| undefined = useMemo(() => {
if (!("outputs" in run)) return undefined;
if (!["running", "success", "failed"].includes(selectedRunStatus))
return null;
// Add type info from agent input schema
return Object.fromEntries(
Object.entries(run.outputs).map(([k, v]) => [
k,
{ value: v, type: agent.output_schema.properties[k].type },
]),
);
}, [agent, run, selectedRunStatus]);
const runActions: { label: string; callback: () => void }[] = useMemo(
() => [{ label: "Run again", callback: () => runAgain() }],
[runAgain],
);
return (
<div className="agpt-div flex gap-6">
<div className="flex flex-1 flex-col gap-4">
<Card className="agpt-box">
<CardHeader>
<CardTitle className="font-poppins text-lg">Info</CardTitle>
</CardHeader>
<CardContent>
<div className="flex justify-stretch gap-4">
{infoStats.map(({ label, value }) => (
<div key={label} className="flex-1">
<p className="text-sm font-medium text-black">{label}</p>
<p className="text-sm text-neutral-600">{value}</p>
</div>
))}
</div>
</CardContent>
</Card>
{agentRunOutputs !== null && (
<Card className="agpt-box">
<CardHeader>
<CardTitle className="font-poppins text-lg">Output</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{agentRunOutputs !== undefined ? (
Object.entries(agentRunOutputs).map(([key, { value }]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">{key}</label>
<pre>{value}</pre>
{/* TODO: pretty type-dependent rendering */}
</div>
))
) : (
<p>Loading...</p>
)}
</CardContent>
</Card>
)}
<Card className="agpt-box">
<CardHeader>
<CardTitle className="font-poppins text-lg">Input</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{agentRunInputs !== undefined ? (
Object.entries(agentRunInputs).map(([key, { value }]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">{key}</label>
<Input
defaultValue={value}
className="rounded-full"
disabled
/>
</div>
))
) : (
<p>Loading...</p>
)}
</CardContent>
</Card>
</div>
{/* 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>
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Agent actions</h3>
{agentActions.map((action, i) => (
<Button key={i} variant="outline" onClick={action.callback}>
{action.label}
</Button>
))}
</div>
</div>
</aside>
</div>
);
}
function AgentScheduleDetailsView({
agent,
schedule,
onForcedRun,
agentActions,
}: {
agent: GraphMeta;
schedule: Schedule;
onForcedRun: (runID: string) => void;
agentActions: { label: string; callback: () => void }[];
}): React.ReactNode {
const api = useBackendAPI();
const selectedRunStatus: AgentRunStatus = "scheduled";
const infoStats: { label: string; value: React.ReactNode }[] = useMemo(() => {
return [
{
label: "Status",
value:
selectedRunStatus.charAt(0).toUpperCase() +
selectedRunStatus.slice(1),
},
{ label: "Scheduled for", value: schedule.next_run_time },
];
}, [schedule, selectedRunStatus]);
const agentRunInputs: Record<string, { type: BlockIOSubType; value: any }> =
useMemo(() => {
// TODO: show (link to) preset - https://github.com/Significant-Gravitas/AutoGPT/issues/9168
// Add type info from agent input schema
return Object.fromEntries(
Object.entries(schedule.input_data).map(([k, v]) => [
k,
{ value: v, type: agent.input_schema.properties[k].type },
]),
);
}, [agent, schedule]);
const runNow = useCallback(
() =>
api
.executeGraph(agent.id, schedule.input_data)
.then((run) => onForcedRun(run.id)),
[api, agent, schedule, onForcedRun],
);
const runActions: { label: string; callback: () => void }[] = useMemo(
() => [{ label: "Run now", callback: () => runNow() }],
[runNow],
);
return (
<div className="agpt-div flex gap-6">
<div className="flex flex-1 flex-col gap-4">
<Card className="agpt-box">
<CardHeader>
<CardTitle className="font-poppins text-lg">Info</CardTitle>
</CardHeader>
<CardContent>
<div className="flex justify-stretch gap-4">
{infoStats.map(({ label, value }) => (
<div key={label} className="flex-1">
<p className="text-sm font-medium text-black">{label}</p>
<p className="text-sm text-neutral-600">{value}</p>
</div>
))}
</div>
</CardContent>
</Card>
<Card className="agpt-box">
<CardHeader>
<CardTitle className="font-poppins text-lg">Input</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{agentRunInputs !== undefined ? (
Object.entries(agentRunInputs).map(([key, { value }]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">{key}</label>
<Input
defaultValue={value}
className="rounded-full"
disabled
/>
</div>
))
) : (
<p>Loading...</p>
)}
</CardContent>
</Card>
</div>
{/* 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>
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Agent actions</h3>
{agentActions.map((action, i) => (
<Button key={i} variant="outline" onClick={action.callback}>
{action.label}
</Button>
))}
</div>
</div>
</aside>
</div>
);
}
function AgentRunDraftView({
agent,
onRun,
agentActions,
}: {
agent: GraphMeta;
onRun: (runID: string) => void;
agentActions: { label: string; callback: () => void }[];
}): React.ReactNode {
const api = useBackendAPI();
const agentInputs = agent.input_schema.properties;
const [inputValues, setInputValues] = useState<Record<string, any>>({});
const doRun = useCallback(
() =>
api
.executeGraph(agent.id, inputValues)
.then((newRun) => onRun(newRun.id)),
[api, agent, inputValues, onRun],
);
const runActions: {
label: string;
variant?: ButtonProps["variant"];
callback: () => void;
}[] = useMemo(
() => [{ label: "Run", variant: "accent", callback: () => doRun() }],
[doRun],
);
return (
<div className="agpt-div flex gap-6">
<div className="flex flex-1 flex-col gap-4">
<Card className="agpt-box">
<CardHeader>
<CardTitle className="font-poppins text-lg">Input</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{Object.entries(agentInputs).map(([key, inputSubSchema]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">{key}</label>
<Input
defaultValue={
"default" in inputSubSchema ? inputSubSchema.default : ""
}
className="rounded-full"
onChange={(e) =>
setInputValues((obj) => ({ ...obj, [key]: e.target.value }))
}
/>
</div>
))}
</CardContent>
</Card>
</div>
{/* 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>
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Agent actions</h3>
{agentActions.map((action, i) => (
<Button key={i} variant="outline" onClick={action.callback}>
{action.label}
</Button>
))}
</div>
</div>
</aside>
</div>
);
}

View File

@ -2,9 +2,54 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base {
.font-neue {
font-family: "PP Neue Montreal TT", sans-serif;
}
}
@layer utilities {
.w-110 {
width: 27.5rem;
}
.h-7\.5 {
height: 1.1875rem;
}
.h-18 {
height: 4.5rem;
}
.h-238 {
height: 14.875rem;
}
.top-158 {
top: 9.875rem;
}
.top-254 {
top: 15.875rem;
}
.top-284 {
top: 17.75rem;
}
.top-360 {
top: 22.5rem;
}
.left-297 {
left: 18.5625rem;
}
.left-34 {
left: 2.125rem;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@layer base { @layer base {
:root { :root {
--background: 250 5% 98%; --background: 0 0% 100%;
--foreground: 240 10% 3.9%; --foreground: 240 10% 3.9%;
--card: 0 0% 100%; --card: 0 0% 100%;
--card-foreground: 240 10% 3.9%; --card-foreground: 240 10% 3.9%;
@ -16,8 +61,8 @@
--secondary-foreground: 240 5.9% 10%; --secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%; --muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%; --muted-foreground: 240 3.8% 46.1%;
--accent: 262 83% 58%; --accent: 240 4.8% 95.9%;
--accent-foreground: 0 0% 100%; --accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%; --border: 240 5.9% 90%;
@ -57,7 +102,9 @@
--chart-4: 280 65% 60%; --chart-4: 280 65% 60%;
--chart-5: 340 75% 55%; --chart-5: 340 75% 55%;
} }
}
@layer base {
* { * {
@apply border-border; @apply border-border;
} }
@ -65,14 +112,6 @@
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
.font-neue {
font-family: "PP Neue Montreal TT", sans-serif;
}
}
/* *** AutoGPT Design Components *** */
@layer components {
.agpt-border-input { .agpt-border-input {
@apply border border-input focus-visible:border-gray-400 focus-visible:outline-none; @apply border border-input focus-visible:border-gray-400 focus-visible:outline-none;
} }
@ -80,67 +119,4 @@
.agpt-shadow-input { .agpt-shadow-input {
@apply shadow-sm focus-visible:shadow-md; @apply shadow-sm focus-visible:shadow-md;
} }
.agpt-rounded-card {
@apply rounded-2xl;
}
.agpt-rounded-box {
@apply rounded-3xl;
}
.agpt-card {
@apply agpt-rounded-card border border-zinc-300 bg-white p-[1px];
}
.agpt-box {
@apply agpt-card agpt-rounded-box;
}
.agpt-div {
@apply border-zinc-200 p-5;
}
}
@layer utilities {
.agpt-card-selected {
@apply border-2 border-accent bg-violet-50/50 p-0;
}
}
@layer utilities {
/* TODO: 1. remove unused utility classes */
/* TODO: 2. fix naming of numbered dimensions so that the number is 4*dimension */
/* TODO: 3. move to tailwind.config.ts spacing config */
.h-7\.5 {
height: 1.1875rem;
}
.h-18 {
height: 4.5rem;
}
.h-238 {
height: 14.875rem;
}
.top-158 {
top: 9.875rem;
}
.top-254 {
top: 15.875rem;
}
.top-284 {
top: 17.75rem;
}
.top-360 {
top: 22.5rem;
}
.left-297 {
left: 18.5625rem;
}
.left-34 {
left: 2.125rem;
}
.text-balance {
text-wrap: balance;
}
} }

View File

@ -102,7 +102,7 @@ export default async function RootLayout({
}, },
]} ]}
/> />
<main className="w-full flex-1">{children}</main> <main className="flex-1">{children}</main>
<TallyPopupSimple /> <TallyPopupSimple />
</div> </div>
<Toaster /> <Toaster />

View File

@ -93,61 +93,59 @@ export default function LoginPage() {
} }
return ( return (
<div className="flex justify-center"> <AuthCard>
<AuthCard> <AuthHeader>Login to your account</AuthHeader>
<AuthHeader>Login to your account</AuthHeader> <Form {...form}>
<Form {...form}> <form onSubmit={form.handleSubmit(onLogin)}>
<form onSubmit={form.handleSubmit(onLogin)}> <FormField
<FormField control={form.control}
control={form.control} name="email"
name="email" render={({ field }) => (
render={({ field }) => ( <FormItem className="mb-6">
<FormItem className="mb-6"> <FormLabel>Email</FormLabel>
<FormLabel>Email</FormLabel> <FormControl>
<FormControl> <Input placeholder="m@example.com" {...field} />
<Input placeholder="m@example.com" {...field} /> </FormControl>
</FormControl> <FormMessage />
<FormMessage /> </FormItem>
</FormItem> )}
)} />
/> <FormField
<FormField control={form.control}
control={form.control} name="password"
name="password" render={({ field }) => (
render={({ field }) => ( <FormItem className="mb-6">
<FormItem className="mb-6"> <FormLabel className="flex w-full items-center justify-between">
<FormLabel className="flex w-full items-center justify-between"> <span>Password</span>
<span>Password</span> <Link
<Link href="/reset_password"
href="/reset_password" className="text-sm font-normal leading-normal text-black underline"
className="text-sm font-normal leading-normal text-black underline" >
> Forgot your password?
Forgot your password? </Link>
</Link> </FormLabel>
</FormLabel> <FormControl>
<FormControl> <PasswordInput {...field} />
<PasswordInput {...field} /> </FormControl>
</FormControl> <FormMessage />
<FormMessage /> </FormItem>
</FormItem> )}
)} />
/> <AuthButton
<AuthButton onClick={() => onLogin(form.getValues())}
onClick={() => onLogin(form.getValues())} isLoading={isLoading}
isLoading={isLoading} type="submit"
type="submit" >
> Login
Login </AuthButton>
</AuthButton> </form>
</form> <AuthFeedback message={feedback} isError={true} />
<AuthFeedback message={feedback} isError={true} /> </Form>
</Form> <AuthBottomText
<AuthBottomText text="Don't have an account?"
text="Don't have an account?" linkText="Sign up"
linkText="Sign up" href="/signup"
href="/signup" />
/> </AuthCard>
</AuthCard>
</div>
); );
} }

View File

@ -1,11 +1,7 @@
"use client"; "use client";
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { import { GraphExecution, Schedule, GraphMeta } from "@/lib/autogpt-server-api";
GraphExecutionMeta,
Schedule,
GraphMeta,
} from "@/lib/autogpt-server-api";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { import {
@ -20,12 +16,10 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context";
const Monitor = () => { const Monitor = () => {
const [flows, setFlows] = useState<GraphMeta[]>([]); const [flows, setFlows] = useState<GraphMeta[]>([]);
const [executions, setExecutions] = useState<GraphExecutionMeta[]>([]); const [executions, setExecutions] = useState<GraphExecution[]>([]);
const [schedules, setSchedules] = useState<Schedule[]>([]); const [schedules, setSchedules] = useState<Schedule[]>([]);
const [selectedFlow, setSelectedFlow] = useState<GraphMeta | null>(null); const [selectedFlow, setSelectedFlow] = useState<GraphMeta | null>(null);
const [selectedRun, setSelectedRun] = useState<GraphExecutionMeta | null>( const [selectedRun, setSelectedRun] = useState<GraphExecution | null>(null);
null,
);
const [sortColumn, setSortColumn] = useState<keyof Schedule>("id"); const [sortColumn, setSortColumn] = useState<keyof Schedule>("id");
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
const api = useBackendAPI(); const api = useBackendAPI();
@ -79,7 +73,7 @@ const Monitor = () => {
return ( return (
<div <div
className="grid grid-cols-1 gap-4 p-4 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10" className="grid grid-cols-1 gap-4 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10"
data-testid="monitor-page" data-testid="monitor-page"
> >
<AgentFlowList <AgentFlowList

View File

@ -84,138 +84,136 @@ export default function SignupPage() {
} }
return ( return (
<div className="flex justify-center"> <AuthCard>
<AuthCard> <AuthHeader>Create a new account</AuthHeader>
<AuthHeader>Create a new account</AuthHeader> <Form {...form}>
<Form {...form}> <form onSubmit={form.handleSubmit(onSignup)}>
<form onSubmit={form.handleSubmit(onSignup)}> <FormField
<FormField control={form.control}
control={form.control} name="email"
name="email" render={({ field }) => (
render={({ field }) => ( <FormItem className="mb-6">
<FormItem className="mb-6"> <FormLabel>Email</FormLabel>
<FormLabel>Email</FormLabel> <FormControl>
<FormControl> <Input placeholder="m@example.com" {...field} />
<Input placeholder="m@example.com" {...field} /> </FormControl>
</FormControl> <FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem className="mb-6">
<FormLabel>Password</FormLabel>
<FormControl>
<PasswordInput {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem className="mb-4">
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<PasswordInput {...field} />
</FormControl>
<FormDescription className="text-sm font-normal leading-tight text-slate-500">
Password needs to be at least 6 characters long
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<AuthButton
onClick={() => onSignup(form.getValues())}
isLoading={isLoading}
type="submit"
>
Sign up
</AuthButton>
<FormField
control={form.control}
name="agreeToTerms"
render={({ field }) => (
<FormItem className="mt-6 flex flex-row items-start -space-y-1 space-x-2">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="">
<FormLabel>
<span className="mr-1 text-sm font-normal leading-normal text-slate-950">
I agree to the
</span>
<Link
href="https://auto-gpt.notion.site/Terms-of-Use-11400ef5bece80d0b087d7831c5fd6bf"
className="text-sm font-normal leading-normal text-slate-950 underline"
>
Terms of Use
</Link>
<span className="mx-1 text-sm font-normal leading-normal text-slate-950">
and
</span>
<Link
href="https://www.notion.so/auto-gpt/Privacy-Policy-ab11c9c20dbd4de1a15dcffe84d77984"
className="text-sm font-normal leading-normal text-slate-950 underline"
>
Privacy Policy
</Link>
</FormLabel>
<FormMessage /> <FormMessage />
</FormItem> </div>
)} </FormItem>
/> )}
<FormField />
control={form.control} </form>
name="password" <AuthFeedback message={feedback} isError={true} />
render={({ field }) => ( </Form>
<FormItem className="mb-6"> {showWaitlistPrompt && (
<FormLabel>Password</FormLabel> <div>
<FormControl> <span className="mr-1 text-sm font-normal leading-normal text-red-500">
<PasswordInput {...field} /> The provided email may not be allowed to sign up.
</FormControl> </span>
<FormMessage /> <br />
</FormItem> <span className="mx-1 text-sm font-normal leading-normal text-slate-950">
)} - AutoGPT Platform is currently in closed beta. You can join
/> </span>
<FormField <Link
control={form.control} href="https://agpt.co/waitlist"
name="confirmPassword" className="text-sm font-normal leading-normal text-slate-950 underline"
render={({ field }) => ( >
<FormItem className="mb-4"> the waitlist here.
<FormLabel>Confirm Password</FormLabel> </Link>
<FormControl> <br />
<PasswordInput {...field} /> <span className="mx-1 text-sm font-normal leading-normal text-slate-950">
</FormControl> - Make sure you use the same email address you used to sign up for
<FormDescription className="text-sm font-normal leading-tight text-slate-500"> the waitlist.
Password needs to be at least 6 characters long </span>
</FormDescription> <br />
<FormMessage /> <span className="mx-1 text-sm font-normal leading-normal text-slate-950">
</FormItem> - You can self host the platform, visit our
)} </span>
/> <Link
<AuthButton href="https://agpt.co/waitlist"
onClick={() => onSignup(form.getValues())} className="text-sm font-normal leading-normal text-slate-950 underline"
isLoading={isLoading} >
type="submit" GitHub repository.
> </Link>
Sign up </div>
</AuthButton> )}
<FormField <AuthBottomText
control={form.control} text="Already a member?"
name="agreeToTerms" linkText="Log in"
render={({ field }) => ( href="/login"
<FormItem className="mt-6 flex flex-row items-start -space-y-1 space-x-2"> />
<FormControl> </AuthCard>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="">
<FormLabel>
<span className="mr-1 text-sm font-normal leading-normal text-slate-950">
I agree to the
</span>
<Link
href="https://auto-gpt.notion.site/Terms-of-Use-11400ef5bece80d0b087d7831c5fd6bf"
className="text-sm font-normal leading-normal text-slate-950 underline"
>
Terms of Use
</Link>
<span className="mx-1 text-sm font-normal leading-normal text-slate-950">
and
</span>
<Link
href="https://www.notion.so/auto-gpt/Privacy-Policy-ab11c9c20dbd4de1a15dcffe84d77984"
className="text-sm font-normal leading-normal text-slate-950 underline"
>
Privacy Policy
</Link>
</FormLabel>
<FormMessage />
</div>
</FormItem>
)}
/>
</form>
<AuthFeedback message={feedback} isError={true} />
</Form>
{showWaitlistPrompt && (
<div>
<span className="mr-1 text-sm font-normal leading-normal text-red-500">
The provided email may not be allowed to sign up.
</span>
<br />
<span className="mx-1 text-sm font-normal leading-normal text-slate-950">
- AutoGPT Platform is currently in closed beta. You can join
</span>
<Link
href="https://agpt.co/waitlist"
className="text-sm font-normal leading-normal text-slate-950 underline"
>
the waitlist here.
</Link>
<br />
<span className="mx-1 text-sm font-normal leading-normal text-slate-950">
- Make sure you use the same email address you used to sign up for
the waitlist.
</span>
<br />
<span className="mx-1 text-sm font-normal leading-normal text-slate-950">
- You can self host the platform, visit our
</span>
<Link
href="https://agpt.co/waitlist"
className="text-sm font-normal leading-normal text-slate-950 underline"
>
GitHub repository.
</Link>
</div>
)}
<AuthBottomText
text="Already a member?"
linkText="Log in"
href="/login"
/>
</AuthCard>
</div>
); );
} }

View File

@ -85,6 +85,7 @@ export default function Page({}: {}) {
<PublishAgentPopout <PublishAgentPopout
trigger={ trigger={
<Button <Button
variant="default"
size="sm" size="sm"
onClick={onOpenPopout} onClick={onOpenPopout}
className="h-9 rounded-full bg-black px-4 text-sm font-medium text-white hover:bg-neutral-700 dark:hover:bg-neutral-600" className="h-9 rounded-full bg-black px-4 text-sm font-medium text-white hover:bg-neutral-700 dark:hover:bg-neutral-600"

View File

@ -92,6 +92,7 @@ export const AgentImageItem: React.FC<AgentImageItemProps> = React.memo(
{isVideoFile && playingVideoIndex !== index && ( {isVideoFile && playingVideoIndex !== index && (
<div className="absolute bottom-2 left-2 sm:bottom-3 sm:left-3 md:bottom-4 md:left-4 lg:bottom-[1.25rem] lg:left-[1.25rem]"> <div className="absolute bottom-2 left-2 sm:bottom-3 sm:left-3 md:bottom-4 md:left-4 lg:bottom-[1.25rem] lg:left-[1.25rem]">
<Button <Button
variant="default"
size="default" size="default"
onClick={() => { onClick={() => {
if (videoRef.current) { if (videoRef.current) {

View File

@ -95,7 +95,7 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
return ( return (
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0"> <div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">
{/* Title */} {/* Title */}
<div className="mb-3 w-full font-poppins text-2xl font-medium leading-normal text-neutral-900 dark:text-neutral-100 sm:text-3xl lg:mb-4 lg:text-[35px] lg:leading-10"> <div className="font-poppins mb-3 w-full text-2xl font-medium leading-normal text-neutral-900 dark:text-neutral-100 sm:text-3xl lg:mb-4 lg:text-[35px] lg:leading-10">
{name} {name}
</div> </div>

View File

@ -1,50 +0,0 @@
import React from "react";
import { Badge } from "@/components/ui/badge";
export type AgentRunStatus =
| "success"
| "failed"
| "queued"
| "running"
| "stopped"
| "scheduled"
| "draft";
const statusData: Record<
AgentRunStatus,
{ label: string; variant: keyof typeof statusStyles }
> = {
success: { label: "Success", variant: "success" },
running: { label: "Running", variant: "info" },
failed: { label: "Failed", variant: "destructive" },
queued: { label: "Queued", variant: "warning" },
draft: { label: "Draft", variant: "secondary" },
stopped: { label: "Stopped", variant: "secondary" },
scheduled: { label: "Scheduled", variant: "secondary" },
};
const statusStyles = {
success:
"bg-green-100 text-green-800 hover:bg-green-100 hover:text-green-800",
destructive: "bg-red-100 text-red-800 hover:bg-red-100 hover:text-red-800",
warning:
"bg-yellow-100 text-yellow-800 hover:bg-yellow-100 hover:text-yellow-800",
info: "bg-blue-100 text-blue-800 hover:bg-blue-100 hover:text-blue-800",
secondary:
"bg-slate-100 text-slate-800 hover:bg-slate-100 hover:text-slate-800",
};
export default function AgentRunStatusChip({
status,
}: {
status: AgentRunStatus;
}): React.ReactElement {
return (
<Badge
variant="secondary"
className={`text-xs font-medium ${statusStyles[statusData[status].variant]} rounded-[45px] px-[9px] py-[3px]`}
>
{statusData[status].label}
</Badge>
);
}

View File

@ -1,80 +0,0 @@
import React from "react";
import moment from "moment";
import { MoreVertical } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import AgentRunStatusChip, { AgentRunStatus } from "./AgentRunStatusChip";
export type AgentRunSummaryProps = {
agentID: string;
agentRunID: string;
status: AgentRunStatus;
title: string;
timestamp: number | Date;
selected?: boolean;
onClick?: () => void;
className?: string;
};
export default function AgentRunSummaryCard({
agentID,
agentRunID,
status,
title,
timestamp,
selected = false,
onClick,
className,
}: AgentRunSummaryProps): React.ReactElement {
return (
<Card
className={cn(
"agpt-rounded-card cursor-pointer border-zinc-300",
selected ? "agpt-card-selected" : "",
className,
)}
onClick={onClick}
>
<CardContent className="relative p-2.5 lg:p-4">
<AgentRunStatusChip status={status} />
<div className="mt-5 flex items-center justify-between">
<h3 className="truncate pr-2 text-base font-medium text-neutral-900">
{title}
</h3>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-5 w-5 p-0">
<MoreVertical className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem /* TODO: implement */>
Pin into a template
</DropdownMenuItem>
<DropdownMenuItem /* TODO: implement */>Rename</DropdownMenuItem>
<DropdownMenuItem /* TODO: implement */>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<p
className="mt-1 text-sm font-normal text-neutral-500"
title={moment(timestamp).toString()}
>
Ran {moment(timestamp).fromNow()}
</p>
</CardContent>
</Card>
);
}

View File

@ -26,13 +26,13 @@ export const BecomeACreator: React.FC<BecomeACreatorProps> = ({
<div className="left-0 top-0 h-px w-full bg-gray-200 dark:bg-gray-700" /> <div className="left-0 top-0 h-px w-full bg-gray-200 dark:bg-gray-700" />
{/* Title */} {/* Title */}
<h2 className="underline-from-font decoration-skip-ink-none mb-[77px] mt-[25px] text-left font-poppins text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200"> <h2 className="font-poppins underline-from-font decoration-skip-ink-none mb-[77px] mt-[25px] text-left text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
{title} {title}
</h2> </h2>
{/* Content Container */} {/* Content Container */}
<div className="absolute left-1/2 top-1/2 w-full max-w-[900px] -translate-x-1/2 -translate-y-1/2 px-4 pt-16 text-center md:px-6 lg:px-0"> <div className="absolute left-1/2 top-1/2 w-full max-w-[900px] -translate-x-1/2 -translate-y-1/2 px-4 pt-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"> <h2 className="font-poppins underline-from-font decoration-skip-ink-none mb-6 text-center 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 Build AI agents and share
<br /> <br />
<span className="text-violet-600 dark:text-violet-400"> <span className="text-violet-600 dark:text-violet-400">
@ -51,7 +51,7 @@ export const BecomeACreator: React.FC<BecomeACreatorProps> = ({
onClick={handleButtonClick} onClick={handleButtonClick}
className="inline-flex h-[48px] cursor-pointer items-center justify-center rounded-[38px] bg-neutral-800 px-8 py-3 transition-colors hover:bg-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-600 md:h-[56px] md:px-10 md:py-4 lg:h-[68px] lg:px-12 lg:py-5" className="inline-flex h-[48px] cursor-pointer items-center justify-center rounded-[38px] bg-neutral-800 px-8 py-3 transition-colors hover:bg-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-600 md:h-[56px] md:px-10 md:py-4 lg:h-[68px] lg:px-12 lg:py-5"
> >
<span className="whitespace-nowrap font-poppins text-base font-medium leading-normal text-neutral-50 md:text-lg md:leading-relaxed lg:text-xl lg:leading-7"> <span className="font-poppins whitespace-nowrap text-base font-medium leading-normal text-neutral-50 md:text-lg md:leading-relaxed lg:text-xl lg:leading-7">
{buttonText} {buttonText}
</span> </span>
</button> </button>

View File

@ -65,12 +65,15 @@ export const Interactive: Story = {
export const Variants: Story = { export const Variants: Story = {
render: (args) => ( render: (args) => (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Button {...args} variant="outline"> <Button {...args} variant="default">
Outline (default) Default
</Button> </Button>
<Button {...args} variant="destructive"> <Button {...args} variant="destructive">
Destructive Destructive
</Button> </Button>
<Button {...args} variant="outline">
Outline
</Button>
<Button {...args} variant="secondary"> <Button {...args} variant="secondary">
Secondary Secondary
</Button> </Button>

View File

@ -5,15 +5,16 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-neutral-300 font-neue leading-9 tracking-tight", "inline-flex items-center justify-center whitespace-nowrap rounded-[80px] text-xl font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-neutral-300 font-neue leading-9 tracking-tight",
{ {
variants: { variants: {
variant: { variant: {
default:
"bg-white border border-black/50 text-[#272727] hover:bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700",
destructive: destructive:
"bg-red-600 text-neutral-50 border border-red-500/50 hover:bg-red-500/90 dark:bg-red-700 dark:text-neutral-50 dark:hover:bg-red-600", "bg-red-600 text-neutral-50 border border-red-500/50 hover:bg-red-500/90 dark:bg-red-700 dark:text-neutral-50 dark:hover:bg-red-600",
accent: "bg-accent text-accent-foreground hover:bg-violet-500",
outline: outline:
"border border-black/50 text-[#272727] hover:bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700", "bg-white border border-black/50 text-[#272727] hover:bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700",
secondary: secondary:
"bg-neutral-100 text-[#272727] border border-neutral-200 hover:bg-neutral-100/80 dark:bg-neutral-700 dark:text-neutral-100 dark:border-neutral-600 dark:hover:bg-neutral-600", "bg-neutral-100 text-[#272727] border border-neutral-200 hover:bg-neutral-100/80 dark:bg-neutral-700 dark:text-neutral-100 dark:border-neutral-600 dark:hover:bg-neutral-600",
ghost: ghost:
@ -21,17 +22,17 @@ const buttonVariants = cva(
link: "text-[#272727] underline-offset-4 hover:underline dark:text-neutral-100", link: "text-[#272727] underline-offset-4 hover:underline dark:text-neutral-100",
}, },
size: { size: {
default: "h-10 px-4 py-2 rounded-full text-sm", default:
sm: "h-8 px-3 py-1.5 rounded-full text-xs", "h-10 px-4 py-2 text-sm sm:h-12 sm:px-5 sm:py-2.5 sm:text-base md:h-14 md:px-6 md:py-3 md:text-lg lg:h-[4.375rem] lg:px-[1.625rem] lg:py-[0.4375rem] lg:text-xl",
lg: "h-12 px-5 py-2.5 rounded-full text-lg", sm: "h-8 px-3 py-1.5 text-xs sm:h-9 sm:px-3.5 sm:py-2 sm:text-sm md:h-10 md:px-4 md:py-2 md:text-base lg:h-[3.125rem] lg:px-[1.25rem] lg:py-[0.3125rem] lg:text-sm",
lg: "h-12 px-5 py-2.5 text-lg sm:h-14 sm:px-6 sm:py-3 sm:text-xl md:h-16 md:px-7 md:py-3.5 md:text-2xl lg:h-[5.625rem] lg:px-[2rem] lg:py-[0.5625rem] lg:text-2xl",
primary: primary:
"h-10 w-28 rounded-full sm:h-12 sm:w-32 md:h-[4.375rem] md:w-[11rem] lg:h-[3.125rem] lg:w-[7rem]", "h-10 w-28 sm:h-12 sm:w-32 md:h-[4.375rem] md:w-[11rem] lg:h-[3.125rem] lg:w-[7rem]",
icon: "h-10 w-10", icon: "h-10 w-10 sm:h-12 sm:w-12 md:h-14 md:w-14 lg:h-[4.375rem] lg:w-[4.375rem]",
card: "h-12 p-5 agpt-rounded-card justify-center text-lg",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "outline", variant: "default",
size: "default", size: "default",
}, },
}, },
@ -42,13 +43,13 @@ export interface ButtonProps
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean; asChild?: boolean;
variant?: variant?:
| "default"
| "destructive" | "destructive"
| "accent"
| "outline" | "outline"
| "secondary" | "secondary"
| "ghost" | "ghost"
| "link"; | "link";
size?: "default" | "sm" | "lg" | "primary" | "icon" | "card"; size?: "default" | "sm" | "lg" | "primary" | "icon";
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(

View File

@ -33,7 +33,7 @@ export const CreatorInfoCard: React.FC<CreatorInfoCardProps> = ({
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="flex w-full flex-col items-start justify-start gap-1.5"> <div className="flex w-full flex-col items-start justify-start gap-1.5">
<div className="w-full font-poppins text-[35px] font-medium leading-10 text-neutral-900 dark:text-neutral-100 sm:text-[35px] sm:leading-10"> <div className="font-poppins w-full text-[35px] font-medium leading-10 text-neutral-900 dark:text-neutral-100 sm:text-[35px] sm:leading-10">
{username} {username}
</div> </div>
<div className="font-geist w-full text-lg font-normal leading-6 text-neutral-800 dark:text-neutral-200 sm:text-xl sm:leading-7"> <div className="font-geist w-full text-lg font-normal leading-6 text-neutral-800 dark:text-neutral-200 sm:text-xl sm:leading-7">

View File

@ -34,7 +34,7 @@ export const FeaturedStoreCard: React.FC<FeaturedStoreCardProps> = ({
data-testid="featured-store-card" data-testid="featured-store-card"
> >
<div className="flex h-[188px] flex-col items-start justify-start gap-3 self-stretch"> <div className="flex h-[188px] flex-col items-start justify-start gap-3 self-stretch">
<h2 className="self-stretch font-poppins text-[35px] font-medium leading-10 text-neutral-900 dark:text-neutral-100"> <h2 className="font-poppins self-stretch text-[35px] font-medium leading-10 text-neutral-900 dark:text-neutral-100">
{agentName} {agentName}
</h2> </h2>
<div className="font-lead self-stretch text-xl font-normal leading-7 text-neutral-800 dark:text-neutral-200"> <div className="font-lead self-stretch text-xl font-normal leading-7 text-neutral-800 dark:text-neutral-200">

View File

@ -86,6 +86,7 @@ export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
) : ( ) : (
<Link href="/login"> <Link href="/login">
<Button <Button
variant="default"
size="sm" size="sm"
className="flex items-center justify-end space-x-2" className="flex items-center justify-end space-x-2"
> >
@ -132,7 +133,11 @@ export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
href="/login" href="/login"
className="fixed right-4 top-4 z-50 mt-4 inline-flex h-8 items-center justify-end rounded-lg pr-4 md:hidden" className="fixed right-4 top-4 z-50 mt-4 inline-flex h-8 items-center justify-end rounded-lg pr-4 md:hidden"
> >
<Button size="sm" className="flex items-center space-x-2"> <Button
variant="default"
size="sm"
className="flex items-center space-x-2"
>
<IconLogIn className="h-5 w-5" /> <IconLogIn className="h-5 w-5" />
<span>Log In</span> <span>Log In</span>
</Button> </Button>

View File

@ -49,7 +49,7 @@ export const NavbarLink = ({ name, href }: NavbarLinkProps) => {
/> />
)} )}
<div <div
className={`hidden font-poppins text-[20px] font-medium leading-[28px] lg:block ${ className={`font-poppins text-[20px] font-medium leading-[28px] ${
activeLink === href activeLink === href
? "text-neutral-50 dark:text-neutral-900" ? "text-neutral-50 dark:text-neutral-900"
: "text-neutral-900 dark:text-neutral-50" : "text-neutral-900 dark:text-neutral-50"

View File

@ -256,6 +256,7 @@ export const ProfileInfoForm = ({ profile }: { profile: CreatorDetails }) => {
</Button> </Button>
<Button <Button
type="submit" type="submit"
variant="default"
disabled={isSubmitting} disabled={isSubmitting}
className="font-circular h-[50px] rounded-[35px] bg-neutral-800 px-6 py-3 text-base font-medium text-white transition-colors hover:bg-neutral-900 dark:bg-neutral-200 dark:text-neutral-900 dark:hover:bg-neutral-100" className="font-circular h-[50px] rounded-[35px] bg-neutral-800 px-6 py-3 text-base font-medium text-white transition-colors hover:bg-neutral-900 dark:bg-neutral-200 dark:text-neutral-900 dark:hover:bg-neutral-100"
onClick={submitForm} onClick={submitForm}

View File

@ -100,12 +100,14 @@ export const PublishAgentAwaitingReview: React.FC<
<div className="flex w-full flex-col items-center justify-center gap-4 border-t border-slate-200 p-6 dark:border-slate-700 sm:flex-row"> <div className="flex w-full flex-col items-center justify-center gap-4 border-t border-slate-200 p-6 dark:border-slate-700 sm:flex-row">
<Button <Button
onClick={onDone} onClick={onDone}
variant="outline"
className="h-12 w-full rounded-[59px] sm:flex-1" className="h-12 w-full rounded-[59px] sm:flex-1"
> >
Done Done
</Button> </Button>
<Button <Button
onClick={onViewProgress} onClick={onViewProgress}
variant="default"
className="h-12 w-full rounded-[59px] bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-700 dark:text-neutral-100 dark:hover:bg-neutral-600 sm:flex-1" className="h-12 w-full rounded-[59px] bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-700 dark:text-neutral-100 dark:hover:bg-neutral-600 sm:flex-1"
> >
View progress View progress

View File

@ -84,6 +84,7 @@ export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
</div> </div>
<Button <Button
onClick={onOpenBuilder} onClick={onOpenBuilder}
variant="default"
size="lg" size="lg"
className="bg-neutral-800 text-white hover:bg-neutral-900" className="bg-neutral-800 text-white hover:bg-neutral-900"
> >
@ -151,6 +152,7 @@ export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
<div className="flex justify-between gap-4 border-t border-slate-200 p-4 dark:border-slate-700 sm:p-6"> <div className="flex justify-between gap-4 border-t border-slate-200 p-4 dark:border-slate-700 sm:p-6">
<Button <Button
onClick={onCancel} onClick={onCancel}
variant="outline"
size="default" size="default"
className="w-full sm:flex-1" className="w-full sm:flex-1"
> >
@ -163,6 +165,7 @@ export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
} }
}} }}
disabled={!selectedAgentId || !selectedAgentVersion} disabled={!selectedAgentId || !selectedAgentVersion}
variant="default"
size="default" size="default"
className="w-full bg-neutral-800 text-white hover:bg-neutral-900 sm:flex-1" className="w-full bg-neutral-800 text-white hover:bg-neutral-900 sm:flex-1"
> >

View File

@ -342,6 +342,7 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
You can use AI to generate a cover image for you You can use AI to generate a cover image for you
</p> </p>
<Button <Button
variant="default"
size="sm" size="sm"
className={`bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-600 dark:hover:bg-neutral-500 ${ className={`bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-600 dark:hover:bg-neutral-500 ${
images.length >= 5 ? "cursor-not-allowed opacity-50" : "" images.length >= 5 ? "cursor-not-allowed opacity-50" : ""
@ -423,6 +424,7 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
<div className="flex justify-between gap-4 border-t border-slate-200 p-6 dark:border-slate-700"> <div className="flex justify-between gap-4 border-t border-slate-200 p-6 dark:border-slate-700">
<Button <Button
onClick={onBack} onClick={onBack}
variant="outline"
size="default" size="default"
className="w-full dark:border-slate-700 dark:text-slate-300 sm:flex-1" className="w-full dark:border-slate-700 dark:text-slate-300 sm:flex-1"
> >
@ -430,6 +432,7 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
</Button> </Button>
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
variant="default"
size="default" size="default"
className="w-full bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-600 dark:hover:bg-neutral-500 sm:flex-1" className="w-full bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-600 dark:hover:bg-neutral-500 sm:flex-1"
> >

View File

@ -71,7 +71,7 @@ export const StoreCard: React.FC<StoreCardProps> = ({
{/* Content Section */} {/* Content Section */}
<div className="w-full px-2 py-4"> <div className="w-full px-2 py-4">
{/* Title and Creator */} {/* Title and Creator */}
<h3 className="mb-2 font-poppins text-2xl font-semibold leading-tight text-[#272727] dark:text-neutral-100"> <h3 className="font-poppins mb-2 text-2xl font-semibold leading-tight text-[#272727] dark:text-neutral-100">
{agentName} {agentName}
</h3> </h3>
{!hideAvatar && creatorName && ( {!hideAvatar && creatorName && (

View File

@ -46,7 +46,7 @@ export const AgentsSection: React.FC<AgentsSectionProps> = ({
return ( return (
<div className="flex flex-col items-center justify-center py-4 lg:py-8"> <div className="flex flex-col items-center justify-center py-4 lg:py-8">
<div className="w-full max-w-[1360px]"> <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="font-poppins decoration-skip-ink-none mb-8 text-left text-[18px] font-[600] leading-7 text-[#282828] underline-offset-[from-font] dark:text-neutral-200">
{sectionTitle} {sectionTitle}
</div> </div>
{!displayedAgents || displayedAgents.length === 0 ? ( {!displayedAgents || displayedAgents.length === 0 ? (
@ -64,7 +64,7 @@ export const AgentsSection: React.FC<AgentsSectionProps> = ({
> >
<CarouselContent> <CarouselContent>
{displayedAgents.map((agent, index) => ( {displayedAgents.map((agent, index) => (
<CarouselItem key={index} className="min-w-64 max-w-71"> <CarouselItem key={index} className="min-w-64 max-w-68">
<StoreCard <StoreCard
agentName={agent.agent_name} agentName={agent.agent_name}
agentImage={agent.agent_image} agentImage={agent.agent_image}

View File

@ -33,7 +33,7 @@ export const FeaturedCreators: React.FC<FeaturedCreatorsProps> = ({
return ( 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 py-16">
<div className="w-full max-w-[1360px]"> <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"> <h2 className="font-poppins mb-8 text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
{title} {title}
</h2> </h2>

View File

@ -66,7 +66,7 @@ export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
return ( return (
<div className="flex w-full flex-col items-center justify-center"> <div className="flex w-full flex-col items-center justify-center">
<div className="w-[99vw]"> <div className="w-[99vw]">
<h2 className="mx-auto mb-8 max-w-[1360px] px-4 font-poppins text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200"> <h2 className="font-poppins mx-auto mb-8 max-w-[1360px] px-4 text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
Featured agents Featured agents
</h2> </h2>

View File

@ -281,7 +281,7 @@ export const PublishAgentPopout: React.FC<PublishAgentPopoutProps> = ({
}} }}
> >
<PopoverTrigger asChild> <PopoverTrigger asChild>
{trigger || <Button>Publish Agent</Button>} {trigger || <Button variant="default">Publish Agent</Button>}
</PopoverTrigger> </PopoverTrigger>
<PopoverAnchor asChild> <PopoverAnchor asChild>
<div className="fixed left-0 top-0 hidden h-screen w-screen items-center justify-center"></div> <div className="fixed left-0 top-0 hidden h-screen w-screen items-center justify-center"></div>

View File

@ -1,5 +1,5 @@
import BackendAPI, { import BackendAPI, {
GraphExecutionMeta, GraphExecution,
GraphMeta, GraphMeta,
} from "@/lib/autogpt-server-api"; } from "@/lib/autogpt-server-api";
import React, { useMemo } from "react"; import React, { useMemo } from "react";
@ -40,7 +40,7 @@ export const AgentFlowList = ({
className, className,
}: { }: {
flows: GraphMeta[]; flows: GraphMeta[];
executions?: GraphExecutionMeta[]; executions?: GraphExecution[];
selectedFlow: GraphMeta | null; selectedFlow: GraphMeta | null;
onSelectFlow: (f: GraphMeta) => void; onSelectFlow: (f: GraphMeta) => void;
className?: string; className?: string;
@ -109,7 +109,7 @@ export const AgentFlowList = ({
{flows {flows
.map((flow) => { .map((flow) => {
let runCount = 0, let runCount = 0,
lastRun: GraphExecutionMeta | null = null; lastRun: GraphExecution | null = null;
if (executions) { if (executions) {
const _flowRuns = executions.filter( const _flowRuns = executions.filter(
(r) => r.graph_id == flow.id, (r) => r.graph_id == flow.id,

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState, useCallback } from "react"; import React, { useEffect, useState, useCallback } from "react";
import { import {
GraphExecutionMeta, GraphExecution,
Graph, Graph,
GraphMeta, GraphMeta,
safeCopyGraph, safeCopyGraph,
@ -32,6 +32,7 @@ import {
DialogFooter, DialogFooter,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { CronScheduler } from "@/components/cronScheduler";
import RunnerInputUI from "@/components/runner-ui/RunnerInputUI"; import RunnerInputUI from "@/components/runner-ui/RunnerInputUI";
import useAgentGraph from "@/hooks/useAgentGraph"; import useAgentGraph from "@/hooks/useAgentGraph";
import { useBackendAPI } from "@/lib/autogpt-server-api/context"; import { useBackendAPI } from "@/lib/autogpt-server-api/context";
@ -39,7 +40,7 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context";
export const FlowInfo: React.FC< export const FlowInfo: React.FC<
React.HTMLAttributes<HTMLDivElement> & { React.HTMLAttributes<HTMLDivElement> & {
flow: GraphMeta; flow: GraphMeta;
executions: GraphExecutionMeta[]; executions: GraphExecution[];
flowVersion?: number | "all"; flowVersion?: number | "all";
refresh: () => void; refresh: () => void;
} }

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { import {
GraphExecutionMeta, GraphExecution,
GraphMeta, GraphMeta,
NodeExecutionResult, NodeExecutionResult,
SpecialBlockID, SpecialBlockID,
@ -18,7 +18,7 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context";
export const FlowRunInfo: React.FC< export const FlowRunInfo: React.FC<
React.HTMLAttributes<HTMLDivElement> & { React.HTMLAttributes<HTMLDivElement> & {
flow: GraphMeta; flow: GraphMeta;
execution: GraphExecutionMeta; execution: GraphExecution;
} }
> = ({ flow, execution, ...props }) => { > = ({ flow, execution, ...props }) => {
const [isOutputOpen, setIsOutputOpen] = useState(false); const [isOutputOpen, setIsOutputOpen] = useState(false);
@ -26,9 +26,10 @@ export const FlowRunInfo: React.FC<
const api = useBackendAPI(); const api = useBackendAPI();
const fetchBlockResults = useCallback(async () => { const fetchBlockResults = useCallback(async () => {
const executionResults = ( const executionResults = await api.getGraphExecutionInfo(
await api.getGraphExecutionInfo(flow.id, execution.execution_id) flow.id,
).node_executions; execution.execution_id,
);
// Create a map of the latest COMPLETED execution results of output nodes by node_id // Create a map of the latest COMPLETED execution results of output nodes by node_id
const latestCompletedResults = executionResults const latestCompletedResults = executionResults

View File

@ -1,10 +1,10 @@
import React from "react"; import React from "react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { GraphExecutionMeta } from "@/lib/autogpt-server-api"; import { GraphExecution } from "@/lib/autogpt-server-api";
export const FlowRunStatusBadge: React.FC<{ export const FlowRunStatusBadge: React.FC<{
status: GraphExecutionMeta["status"]; status: GraphExecution["status"];
className?: string; className?: string;
}> = ({ status, className }) => ( }> = ({ status, className }) => (
<Badge <Badge

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { GraphExecutionMeta, GraphMeta } from "@/lib/autogpt-server-api"; import { GraphExecution, GraphMeta } from "@/lib/autogpt-server-api";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Table, Table,
@ -15,10 +15,10 @@ import { TextRenderer } from "../ui/render";
export const FlowRunsList: React.FC<{ export const FlowRunsList: React.FC<{
flows: GraphMeta[]; flows: GraphMeta[];
executions: GraphExecutionMeta[]; executions: GraphExecution[];
className?: string; className?: string;
selectedRun?: GraphExecutionMeta | null; selectedRun?: GraphExecution | null;
onSelectRun: (r: GraphExecutionMeta) => void; onSelectRun: (r: GraphExecution) => void;
}> = ({ flows, executions, selectedRun, onSelectRun, className }) => ( }> = ({ flows, executions, selectedRun, onSelectRun, className }) => (
<Card className={className}> <Card className={className}>
<CardHeader> <CardHeader>

View File

@ -1,5 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { GraphExecutionMeta, GraphMeta } from "@/lib/autogpt-server-api"; import { GraphExecution, GraphMeta } from "@/lib/autogpt-server-api";
import { CardTitle } from "@/components/ui/card"; import { CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -12,7 +12,7 @@ import { FlowRunsTimeline } from "@/components/monitor/FlowRunsTimeline";
export const FlowRunsStatus: React.FC<{ export const FlowRunsStatus: React.FC<{
flows: GraphMeta[]; flows: GraphMeta[];
executions: GraphExecutionMeta[]; executions: GraphExecution[];
title?: string; title?: string;
className?: string; className?: string;
}> = ({ flows, executions: executions, title, className }) => { }> = ({ flows, executions: executions, title, className }) => {

View File

@ -1,4 +1,4 @@
import { GraphExecutionMeta, GraphMeta } from "@/lib/autogpt-server-api"; import { GraphExecution, GraphMeta } from "@/lib/autogpt-server-api";
import { import {
ComposedChart, ComposedChart,
DefaultLegendContentProps, DefaultLegendContentProps,
@ -23,7 +23,7 @@ export const FlowRunsTimeline = ({
className, className,
}: { }: {
flows: GraphMeta[]; flows: GraphMeta[];
executions: GraphExecutionMeta[]; executions: GraphExecution[];
dataMin: "dataMin" | number; dataMin: "dataMin" | number;
className?: string; className?: string;
}) => ( }) => (
@ -60,10 +60,8 @@ export const FlowRunsTimeline = ({
<Tooltip <Tooltip
content={({ payload, label }) => { content={({ payload, label }) => {
if (payload && payload.length) { if (payload && payload.length) {
const data: GraphExecutionMeta & { const data: GraphExecution & { time: number; _duration: number } =
time: number; payload[0].payload;
_duration: number;
} = payload[0].payload;
const flow = flows.find((f) => f.id === data.graph_id); const flow = flows.find((f) => f.id === data.graph_id);
return ( return (
<Card className="p-2 text-xs leading-normal"> <Card className="p-2 text-xs leading-normal">

View File

@ -11,7 +11,7 @@ const badgeVariants = cva(
default: default:
"border-transparent bg-neutral-900 text-neutral-50 shadow dark:bg-neutral-50 dark:text-neutral-900", "border-transparent bg-neutral-900 text-neutral-50 shadow dark:bg-neutral-50 dark:text-neutral-900",
secondary: secondary:
"border-transparent bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-50", "border-transparent bg-neutral-100 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-50",
destructive: destructive:
"border-transparent bg-red-500 text-neutral-50 shadow dark:bg-red-900 dark:text-neutral-50", "border-transparent bg-red-500 text-neutral-50 shadow dark:bg-red-900 dark:text-neutral-50",
outline: "text-neutral-950 dark:text-neutral-50", outline: "text-neutral-950 dark:text-neutral-50",

View File

@ -9,7 +9,7 @@ const Card = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"agpt-card text-neutral-950", // TODO: check styling of existing usages "rounded-xl border border-gray-300 bg-white text-neutral-950 shadow",
className, className,
)} )}
{...props} {...props}

View File

@ -1,128 +0,0 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { ChevronDownIcon } from "@radix-ui/react-icons";
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className,
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className,
)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-white px-4 py-2 text-sm font-medium transition-colors hover:bg-neutral-100 hover:text-neutral-900 focus:bg-neutral-100 focus:text-neutral-900 focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-neutral-100/50 data-[state=open]:bg-neutral-100/50 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50 dark:data-[active]:bg-neutral-800/50 dark:data-[state=open]:bg-neutral-800/50",
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}
{""}
<ChevronDownIcon
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
className,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border border-neutral-200 bg-white text-neutral-950 shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50 md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-neutral-200 shadow-md dark:bg-neutral-800" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};

View File

@ -1,55 +0,0 @@
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-neutral-100 p-1 text-neutral-500 dark:bg-neutral-800 dark:text-neutral-400",
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-neutral-950 data-[state=active]:shadow dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300 dark:data-[state=active]:bg-neutral-950 dark:data-[state=active]:text-neutral-50",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300",
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@ -8,7 +8,6 @@ import {
CredentialsDeleteResponse, CredentialsDeleteResponse,
CredentialsMetaResponse, CredentialsMetaResponse,
GraphExecution, GraphExecution,
GraphExecutionMeta,
Graph, Graph,
GraphCreatable, GraphCreatable,
GraphExecuteResponse, GraphExecuteResponse,
@ -97,6 +96,10 @@ export default class BackendAPI {
return this._get(`/graphs`); return this._get(`/graphs`);
} }
getExecutions(): Promise<GraphExecution[]> {
return this._get(`/executions`);
}
getGraph( getGraph(
id: string, id: string,
version?: number, version?: number,
@ -145,37 +148,22 @@ export default class BackendAPI {
return this._request("POST", `/graphs/${id}/execute`, inputData); return this._request("POST", `/graphs/${id}/execute`, inputData);
} }
getExecutions(): Promise<GraphExecutionMeta[]> {
return this._get(`/executions`);
}
getGraphExecutions(graphID: string): Promise<GraphExecutionMeta[]> {
return this._get(`/graphs/${graphID}/executions`);
}
async getGraphExecutionInfo( async getGraphExecutionInfo(
graphID: string, graphID: string,
runID: string, runID: string,
): Promise<GraphExecution> { ): Promise<NodeExecutionResult[]> {
const result = await this._get(`/graphs/${graphID}/executions/${runID}`); return (await this._get(`/graphs/${graphID}/executions/${runID}`)).map(
result.node_executions = result.node_executions.map(
parseNodeExecutionResultTimestamps, parseNodeExecutionResultTimestamps,
); );
return result;
} }
async stopGraphExecution( async stopGraphExecution(
graphID: string, graphID: string,
runID: string, runID: string,
): Promise<GraphExecution> { ): Promise<NodeExecutionResult[]> {
const result = await this._request( return (
"POST", await this._request("POST", `/graphs/${graphID}/executions/${runID}/stop`)
`/graphs/${graphID}/executions/${runID}/stop`, ).map(parseNodeExecutionResultTimestamps);
);
result.node_executions = result.node_executions.map(
parseNodeExecutionResultTimestamps,
);
return result;
} }
oAuthLogin( oAuthLogin(
@ -547,22 +535,7 @@ export default class BackendAPI {
let errorDetail; let errorDetail;
try { try {
const errorData = await response.json(); const errorData = await response.json();
if ( errorDetail = errorData.detail || response.statusText;
Array.isArray(errorData.detail) &&
errorData.detail.length > 0 &&
errorData.detail[0].loc
) {
// This appears to be a Pydantic validation error
const errors = errorData.detail.map(
(err: _PydanticValidationError) => {
const location = err.loc.join(" -> ");
return `${location}: ${err.msg}`;
},
);
errorDetail = errors.join("\n");
} else {
errorDetail = errorData.detail || response.statusText;
}
} catch (e) { } catch (e) {
errorDetail = response.statusText; errorDetail = response.statusText;
} }
@ -744,13 +717,6 @@ type WebsocketMessage = {
}; };
}[keyof WebsocketMessageTypeMap]; }[keyof WebsocketMessageTypeMap];
type _PydanticValidationError = {
type: string;
loc: string[];
msg: string;
input: any;
};
/* *** HELPER FUNCTIONS *** */ /* *** HELPER FUNCTIONS *** */
function parseNodeExecutionResultTimestamps(result: any): NodeExecutionResult { function parseNodeExecutionResultTimestamps(result: any): NodeExecutionResult {

View File

@ -41,8 +41,6 @@ export type BlockIOSubSchema =
| BlockIOSimpleTypeSubSchema | BlockIOSimpleTypeSubSchema
| BlockIOCombinedTypeSubSchema; | BlockIOCombinedTypeSubSchema;
export type BlockIOSubType = BlockIOSimpleTypeSubSchema["type"];
export type BlockIOSimpleTypeSubSchema = export type BlockIOSimpleTypeSubSchema =
| BlockIOObjectSubSchema | BlockIOObjectSubSchema
| BlockIOCredentialsSubSchema | BlockIOCredentialsSubSchema
@ -135,7 +133,7 @@ export const PROVIDER_NAMES = {
export type CredentialsProviderName = export type CredentialsProviderName =
(typeof PROVIDER_NAMES)[keyof typeof PROVIDER_NAMES]; (typeof PROVIDER_NAMES)[keyof typeof PROVIDER_NAMES];
export type BlockIOCredentialsSubSchema = BlockIOObjectSubSchema & { export type BlockIOCredentialsSubSchema = BlockIOSubSchemaMeta & {
/* Mirror of backend/data/model.py:CredentialsFieldSchemaExtra */ /* Mirror of backend/data/model.py:CredentialsFieldSchemaExtra */
credentials_provider: CredentialsProviderName[]; credentials_provider: CredentialsProviderName[];
credentials_scopes?: string[]; credentials_scopes?: string[];
@ -193,8 +191,8 @@ export type LinkCreatable = Omit<Link, "id" | "is_static"> & {
id?: string; id?: string;
}; };
/* Mirror of backend/data/graph.py:GraphExecutionMeta */ /* Mirror of backend/data/graph.py:GraphExecution */
export type GraphExecutionMeta = { export type GraphExecution = {
execution_id: string; execution_id: string;
started_at: number; started_at: number;
ended_at: number; ended_at: number;
@ -203,14 +201,6 @@ export type GraphExecutionMeta = {
status: "QUEUED" | "RUNNING" | "COMPLETED" | "TERMINATED" | "FAILED"; status: "QUEUED" | "RUNNING" | "COMPLETED" | "TERMINATED" | "FAILED";
graph_id: string; graph_id: string;
graph_version: number; graph_version: number;
preset_id?: string;
};
/* Mirror of backend/data/graph.py:GraphExecution */
export type GraphExecution = GraphExecutionMeta & {
inputs: Record<string, any>;
outputs: Record<string, any>;
node_executions: NodeExecutionResult[];
}; };
export type GraphMeta = { export type GraphMeta = {

View File

@ -42,75 +42,39 @@ test.describe("Build", () => { //(1)!
}); });
// --8<-- [end:BuildPageExample] // --8<-- [end:BuildPageExample]
test("user can add all blocks a-l", async ({ page }, testInfo) => { test("user can add all blocks", async ({ page }, testInfo) => {
// this test is slow af so we 10x the timeout (sorry future me) // this test is slow af so we 10x the timeout (sorry future me)
await test.setTimeout(testInfo.timeout * 100); await test.setTimeout(testInfo.timeout * 10);
await test.expect(buildPage.isLoaded()).resolves.toBeTruthy(); await test.expect(buildPage.isLoaded()).resolves.toBeTruthy();
await test.expect(page).toHaveURL(new RegExp("/.*build")); await test.expect(page).toHaveURL(new RegExp("/.*build"));
await buildPage.closeTutorial(); await buildPage.closeTutorial();
await buildPage.openBlocksPanel(); await buildPage.openBlocksPanel();
const blocks = await buildPage.getBlocks(); const blocks = await buildPage.getBlocks();
const blocksToSkip = await buildPage.getBlocksToSkip(); // add all the blocks in order
// add all the blocks in order except for the agent executor block
for (const block of blocks) { for (const block of blocks) {
if (block.name[0].toLowerCase() >= "m") { if (block.id !== "e189baac-8c20-45a1-94a7-55177ea42565") {
continue;
}
if (!blocksToSkip.some((b) => b === block.id)) {
await buildPage.addBlock(block); await buildPage.addBlock(block);
} }
} }
await buildPage.closeBlocksPanel(); await buildPage.closeBlocksPanel();
// check that all the blocks are visible // check that all the blocks are visible
for (const block of blocks) { for (const block of blocks) {
if (block.name[0].toLowerCase() >= "m") { if (block.id !== "e189baac-8c20-45a1-94a7-55177ea42565") {
continue;
}
if (!blocksToSkip.some((b) => b === block.id)) {
console.log("Checking block:", block.name);
await test.expect(buildPage.hasBlock(block)).resolves.toBeTruthy(); await test.expect(buildPage.hasBlock(block)).resolves.toBeTruthy();
} }
} }
// fill in the input for the agent input block
// check that we can save the agent with all the blocks await buildPage.fillBlockInputByPlaceholder(
await buildPage.saveAgent("all blocks test", "all blocks test"); blocks.find((b) => b.name === "Agent Input")?.id ?? "",
// page should have a url like http://localhost:3000/build?flowID=f4f3a1da-cfb3-430f-a074-a455b047e340 "Enter Name",
await test.expect(page).toHaveURL(new RegExp("/.*build\\?flowID=.+")); "Agent Input Field",
}); );
await buildPage.fillBlockInputByPlaceholder(
test("user can add all blocks m-z", async ({ page }, testInfo) => { blocks.find((b) => b.name === "Agent Output")?.id ?? "",
// this test is slow af so we 10x the timeout (sorry future me) "Enter Name",
await test.setTimeout(testInfo.timeout * 100); "Agent Output Field",
await test.expect(buildPage.isLoaded()).resolves.toBeTruthy(); );
await test.expect(page).toHaveURL(new RegExp("/.*build"));
await buildPage.closeTutorial();
await buildPage.openBlocksPanel();
const blocks = await buildPage.getBlocks();
const blocksToSkip = await buildPage.getBlocksToSkip();
// add all the blocks in order except for the agent executor block
for (const block of blocks) {
if (block.name[0].toLowerCase() < "m") {
continue;
}
if (!blocksToSkip.some((b) => b === block.id)) {
await buildPage.addBlock(block);
}
}
await buildPage.closeBlocksPanel();
// check that all the blocks are visible
for (const block of blocks) {
if (block.name[0].toLowerCase() < "m") {
continue;
}
if (!blocksToSkip.some((b) => b === block.id)) {
await test.expect(buildPage.hasBlock(block)).resolves.toBeTruthy();
}
}
// check that we can save the agent with all the blocks // check that we can save the agent with all the blocks
await buildPage.saveAgent("all blocks test", "all blocks test"); await buildPage.saveAgent("all blocks test", "all blocks test");
// page should have a url like http://localhost:3000/build?flowID=f4f3a1da-cfb3-430f-a074-a455b047e340 // page should have a url like http://localhost:3000/build?flowID=f4f3a1da-cfb3-430f-a074-a455b047e340

View File

@ -6,7 +6,8 @@ import { v4 as uuidv4 } from "uuid";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import path from "path"; import path from "path";
// --8<-- [start:AttachAgentId] // --8<-- [start:AttachAgentId]
test.describe("Monitor", () => {
test.describe.skip("Monitor", () => {
let buildPage: BuildPage; let buildPage: BuildPage;
let monitorPage: MonitorPage; let monitorPage: MonitorPage;

View File

@ -1,7 +1,7 @@
import { ElementHandle, Locator, Page } from "@playwright/test"; import { ElementHandle, Locator, Page } from "@playwright/test";
import { BasePage } from "./base.page"; import { BasePage } from "./base.page";
export interface Block { interface Block {
id: string; id: string;
name: string; name: string;
description: string; description: string;
@ -378,39 +378,6 @@ export class BuildPage extends BasePage {
}; };
} }
async getAgentExecutorBlockDetails(): Promise<Block> {
return {
id: "e189baac-8c20-45a1-94a7-55177ea42565",
name: "Agent Executor",
description: "Executes an existing agent inside your agent",
};
}
async getAgentOutputBlockDetails(): Promise<Block> {
return {
id: "363ae599-353e-4804-937e-b2ee3cef3da4",
name: "Agent Output",
description: "This block is used to output the result of an agent.",
};
}
async getAgentInputBlockDetails(): Promise<Block> {
return {
id: "c0a8e994-ebf1-4a9c-a4d8-89d09c86741b",
name: "Agent Input",
description: "This block is used to provide input to the graph.",
};
}
async getGithubTriggerBlockDetails(): Promise<Block> {
return {
id: "6c60ec01-8128-419e-988f-96a063ee2fea",
name: "Github Trigger",
description:
"This block triggers on pull request events and outputs the event type and payload.",
};
}
async nextTutorialStep(): Promise<void> { async nextTutorialStep(): Promise<void> {
console.log(`clicking next tutorial step`); console.log(`clicking next tutorial step`);
await this.page.getByRole("button", { name: "Next" }).click(); await this.page.getByRole("button", { name: "Next" }).click();
@ -481,15 +448,6 @@ export class BuildPage extends BasePage {
); );
} }
async getBlocksToSkip(): Promise<string[]> {
return [
(await this.getAgentExecutorBlockDetails()).id,
(await this.getAgentInputBlockDetails()).id,
(await this.getAgentOutputBlockDetails()).id,
(await this.getGithubTriggerBlockDetails()).id,
];
}
async waitForRunTutorialButton(): Promise<void> { async waitForRunTutorialButton(): Promise<void> {
console.log(`waiting for run tutorial button`); console.log(`waiting for run tutorial button`);
await this.page.waitForSelector('[id="press-run-label"]'); await this.page.waitForSelector('[id="press-run-label"]');

View File

@ -43,6 +43,9 @@ export class MonitorPage extends BasePage {
async isLoaded(): Promise<boolean> { async isLoaded(): Promise<boolean> {
console.log(`checking if monitor page is loaded`); console.log(`checking if monitor page is loaded`);
try { try {
// Wait for network to settle first
await this.page.waitForLoadState("networkidle", { timeout: 10_000 });
// Wait for the monitor page // Wait for the monitor page
await this.page.getByTestId("monitor-page").waitFor({ await this.page.getByTestId("monitor-page").waitFor({
state: "visible", state: "visible",
@ -52,7 +55,7 @@ export class MonitorPage extends BasePage {
// Wait for table headers to be visible (indicates table structure is ready) // Wait for table headers to be visible (indicates table structure is ready)
await this.page.locator("thead th").first().waitFor({ await this.page.locator("thead th").first().waitFor({
state: "visible", state: "visible",
timeout: 15_000, timeout: 5_000,
}); });
// Wait for either a table row or an empty tbody to be present // Wait for either a table row or an empty tbody to be present
@ -60,14 +63,14 @@ export class MonitorPage extends BasePage {
// Wait for at least one row // Wait for at least one row
this.page.locator("tbody tr[data-testid]").first().waitFor({ this.page.locator("tbody tr[data-testid]").first().waitFor({
state: "visible", state: "visible",
timeout: 15_000, timeout: 5_000,
}), }),
// OR wait for an empty tbody (indicating no agents but table is loaded) // OR wait for an empty tbody (indicating no agents but table is loaded)
this.page this.page
.locator("tbody[data-testid='agent-flow-list-body']:empty") .locator("tbody[data-testid='agent-flow-list-body']:empty")
.waitFor({ .waitFor({
state: "visible", state: "visible",
timeout: 15_000, timeout: 5_000,
}), }),
]); ]);

View File

@ -18,7 +18,7 @@ const config = {
mono: ["var(--font-geist-mono)"], mono: ["var(--font-geist-mono)"],
// Include the custom font family // Include the custom font family
neue: ['"PP Neue Montreal TT"', "sans-serif"], neue: ['"PP Neue Montreal TT"', "sans-serif"],
poppins: ["var(--font-poppins)"], poppin: ["var(--font-poppins)"],
inter: ["var(--font-inter)"], inter: ["var(--font-inter)"],
}, },
colors: { colors: {
@ -95,20 +95,26 @@ const config = {
28: "7rem", 28: "7rem",
32: "8rem", 32: "8rem",
36: "9rem", 36: "9rem",
39: "9.875rem",
40: "10rem", 40: "10rem",
44: "11rem", 44: "11rem",
48: "12rem", 48: "12rem",
52: "13rem", 52: "13rem",
56: "14rem", 56: "14rem",
69: "14.875rem",
60: "15rem", 60: "15rem",
63: "15.875rem",
64: "16rem", 64: "16rem",
68: "17rem", 68: "17.75rem",
70: "17.5rem",
71: "17.75rem",
72: "18rem", 72: "18rem",
76: "19rem", 77: "18.5625rem",
80: "20rem", 80: "20rem",
89: "22.5rem",
96: "24rem", 96: "24rem",
110: "27.5rem",
139: "37.1875rem",
167: "41.6875rem",
225: "56.25rem",
}, },
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: "var(--radius)",

View File

@ -2342,26 +2342,6 @@
aria-hidden "^1.1.1" aria-hidden "^1.1.1"
react-remove-scroll "^2.6.1" react-remove-scroll "^2.6.1"
"@radix-ui/react-navigation-menu@^1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.3.tgz#b76b243235acd229b4e00fa61619547d3edf7c99"
integrity sha512-IQWAsQ7dsLIYDrn0WqPU+cdM7MONTv9nqrLVYoie3BPiabSfUVDe6Fr+oEt0Cofsr9ONDcDe9xhmJbL1Uq1yKg==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-collection" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-dismissable-layer" "1.1.3"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-presence" "1.1.2"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-use-previous" "1.1.0"
"@radix-ui/react-visually-hidden" "1.1.1"
"@radix-ui/react-popover@^1.1.4": "@radix-ui/react-popover@^1.1.4":
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.4.tgz#d83104e5fb588870a673b55f3387da4844e5836e" resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.4.tgz#d83104e5fb588870a673b55f3387da4844e5836e"
@ -2522,20 +2502,6 @@
"@radix-ui/react-use-previous" "1.1.0" "@radix-ui/react-use-previous" "1.1.0"
"@radix-ui/react-use-size" "1.1.0" "@radix-ui/react-use-size" "1.1.0"
"@radix-ui/react-tabs@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz#a72da059593cba30fccb30a226d63af686b32854"
integrity sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-presence" "1.1.2"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-roving-focus" "1.1.1"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-toast@^1.2.4": "@radix-ui/react-toast@^1.2.4":
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.2.4.tgz#52fe0e5f169209b7fa300673491a6bedde940279" resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.2.4.tgz#52fe0e5f169209b7fa300673491a6bedde940279"