forked from Significant-Gravitas/AutoGPT
Compare commits
50 Commits
master
...
pwuts/open
Author | SHA1 | Date |
---|---|---|
|
341e4ba1dd | |
|
88e3deda85 | |
|
e3b39b4c95 | |
|
58eeee338c | |
|
a862ba8754 | |
|
3c96e83c91 | |
|
03bbfbc00e | |
|
f547513207 | |
|
f9462bcf2a | |
|
932a4e9a5d | |
|
19bbbc6833 | |
|
6229883cf1 | |
|
694bdd9aa2 | |
|
c5b77e167f | |
|
aa33f6cacc | |
|
46473f5847 | |
|
88f236fe26 | |
|
e8e4a38b84 | |
|
ceffc64790 | |
|
ddbdbd712b | |
|
8c4741d3f9 | |
|
ba6454012c | |
|
fd3a5dd7a2 | |
|
3950f418a6 | |
|
c6a14d30eb | |
|
a8dc3ff222 | |
|
6bec50c931 | |
|
09bcb82126 | |
|
78b2afdcad | |
|
4780b0ad97 | |
|
1ecc073bff | |
|
a8ac41090e | |
|
85180e1c6c | |
|
faf7a42086 | |
|
81a137d852 | |
|
8e82469ae9 | |
|
7ab94c919c | |
|
a5b9dc6024 | |
|
6aaed0aaec | |
|
700f2a3d4a | |
|
577357d6a5 | |
|
d07be7f19e | |
|
3d21a12cf3 | |
|
0da8a3af69 | |
|
f0981cb2e9 | |
|
a2a4833c4e | |
|
a79ab6b914 | |
|
96ddc8c73e | |
|
9d4ba1ea0f | |
|
abc53572cf |
|
@ -2,13 +2,13 @@ name: AutoGPT Platform - Backend CI
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [master, dev, ci-test*]
|
||||
branches: [master, dev, ci-test*, dev-*]
|
||||
paths:
|
||||
- ".github/workflows/platform-backend-ci.yml"
|
||||
- "autogpt_platform/backend/**"
|
||||
- "autogpt_platform/autogpt_libs/**"
|
||||
pull_request:
|
||||
branches: [master, dev, release-*]
|
||||
branches: [master, dev, release-*, dev-*]
|
||||
paths:
|
||||
- ".github/workflows/platform-backend-ci.yml"
|
||||
- "autogpt_platform/backend/**"
|
||||
|
|
|
@ -2,7 +2,7 @@ name: AutoGPT Platform - Frontend CI
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [master, dev]
|
||||
branches: [master, dev, dev-*]
|
||||
paths:
|
||||
- ".github/workflows/platform-frontend-ci.yml"
|
||||
- "autogpt_platform/frontend/**"
|
||||
|
|
|
@ -170,6 +170,16 @@ repos:
|
|||
files: ^classic/benchmark/(agbenchmark|tests)/((?!reports).)*[/.]
|
||||
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
|
||||
# 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.
|
||||
|
@ -221,6 +231,16 @@ repos:
|
|||
language: system
|
||||
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
|
||||
hooks:
|
||||
- id: pytest
|
||||
|
|
|
@ -13,7 +13,6 @@ from typing_extensions import ParamSpec
|
|||
from .config import SETTINGS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
P = ParamSpec("P")
|
||||
T = TypeVar("T")
|
||||
|
|
|
@ -136,6 +136,7 @@ async def create_graph_execution(
|
|||
graph_version: int,
|
||||
nodes_input: list[tuple[str, BlockInput]],
|
||||
user_id: str,
|
||||
preset_id: str | None = None,
|
||||
) -> tuple[str, list[ExecutionResult]]:
|
||||
"""
|
||||
Create a new AgentGraphExecution record.
|
||||
|
@ -163,6 +164,7 @@ async def create_graph_execution(
|
|||
]
|
||||
},
|
||||
"userId": user_id,
|
||||
"agentPresetId": preset_id,
|
||||
},
|
||||
include=GRAPH_EXECUTION_INCLUDE,
|
||||
)
|
||||
|
|
|
@ -6,7 +6,13 @@ from datetime import datetime, timezone
|
|||
from typing import Any, Literal, Optional, Type
|
||||
|
||||
import prisma
|
||||
from prisma.models import AgentGraph, AgentGraphExecution, AgentNode, AgentNodeLink
|
||||
from prisma.models import (
|
||||
AgentGraph,
|
||||
AgentGraphExecution,
|
||||
AgentNode,
|
||||
AgentNodeLink,
|
||||
StoreListingVersion,
|
||||
)
|
||||
from prisma.types import AgentGraphWhereInput
|
||||
from pydantic.fields import computed_field
|
||||
|
||||
|
@ -16,12 +22,15 @@ from backend.util import json
|
|||
|
||||
from .block import BlockInput, BlockType, get_block, get_blocks
|
||||
from .db import BaseDbModel, transaction
|
||||
from .execution import ExecutionStatus
|
||||
from .execution import ExecutionResult, ExecutionStatus
|
||||
from .includes import AGENT_GRAPH_INCLUDE, AGENT_NODE_INCLUDE
|
||||
from .integrations import Webhook
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_INPUT_BLOCK_ID = AgentInputBlock().id
|
||||
_OUTPUT_BLOCK_ID = AgentOutputBlock().id
|
||||
|
||||
|
||||
class Link(BaseDbModel):
|
||||
source_id: str
|
||||
|
@ -100,7 +109,7 @@ class NodeModel(Node):
|
|||
Webhook.model_rebuild()
|
||||
|
||||
|
||||
class GraphExecution(BaseDbModel):
|
||||
class GraphExecutionMeta(BaseDbModel):
|
||||
execution_id: str
|
||||
started_at: datetime
|
||||
ended_at: datetime
|
||||
|
@ -109,33 +118,71 @@ class GraphExecution(BaseDbModel):
|
|||
status: ExecutionStatus
|
||||
graph_id: str
|
||||
graph_version: int
|
||||
preset_id: Optional[str]
|
||||
|
||||
@staticmethod
|
||||
def from_db(execution: AgentGraphExecution):
|
||||
def from_db(_graph_exec: AgentGraphExecution):
|
||||
now = datetime.now(timezone.utc)
|
||||
start_time = execution.startedAt or execution.createdAt
|
||||
end_time = execution.updatedAt or now
|
||||
start_time = _graph_exec.startedAt or _graph_exec.createdAt
|
||||
end_time = _graph_exec.updatedAt or now
|
||||
duration = (end_time - start_time).total_seconds()
|
||||
total_run_time = duration
|
||||
|
||||
try:
|
||||
stats = json.loads(execution.stats or "{}", target_type=dict[str, Any])
|
||||
stats = json.loads(_graph_exec.stats or "{}", target_type=dict[str, Any])
|
||||
except ValueError:
|
||||
stats = {}
|
||||
|
||||
duration = stats.get("walltime", duration)
|
||||
total_run_time = stats.get("nodes_walltime", total_run_time)
|
||||
|
||||
return GraphExecution(
|
||||
id=execution.id,
|
||||
execution_id=execution.id,
|
||||
return GraphExecutionMeta(
|
||||
id=_graph_exec.id,
|
||||
execution_id=_graph_exec.id,
|
||||
started_at=start_time,
|
||||
ended_at=end_time,
|
||||
duration=duration,
|
||||
total_run_time=total_run_time,
|
||||
status=ExecutionStatus(execution.executionStatus),
|
||||
graph_id=execution.agentGraphId,
|
||||
graph_version=execution.agentGraphVersion,
|
||||
status=ExecutionStatus(_graph_exec.executionStatus),
|
||||
graph_id=_graph_exec.agentGraphId,
|
||||
graph_version=_graph_exec.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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -511,17 +558,45 @@ async def get_graphs(
|
|||
return graph_models
|
||||
|
||||
|
||||
async def get_executions(user_id: str) -> list[GraphExecution]:
|
||||
async def get_graphs_executions(user_id: str) -> list[GraphExecutionMeta]:
|
||||
executions = await AgentGraphExecution.prisma().find_many(
|
||||
where={"userId": user_id},
|
||||
order={"createdAt": "desc"},
|
||||
)
|
||||
return [GraphExecution.from_db(execution) for execution in executions]
|
||||
return [GraphExecutionMeta.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:
|
||||
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
|
||||
|
||||
|
@ -529,7 +604,6 @@ async def get_execution(user_id: str, execution_id: str) -> GraphExecution | Non
|
|||
async def get_graph(
|
||||
graph_id: str,
|
||||
version: int | None = None,
|
||||
template: bool = False,
|
||||
user_id: str | None = None,
|
||||
for_export: bool = False,
|
||||
) -> GraphModel | None:
|
||||
|
@ -543,21 +617,36 @@ async def get_graph(
|
|||
where_clause: AgentGraphWhereInput = {
|
||||
"id": graph_id,
|
||||
}
|
||||
|
||||
if version is not None:
|
||||
where_clause["version"] = version
|
||||
elif not template:
|
||||
else:
|
||||
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(
|
||||
where=where_clause,
|
||||
include=AGENT_GRAPH_INCLUDE,
|
||||
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:
|
||||
|
@ -611,9 +700,7 @@ async def create_graph(graph: Graph, user_id: str) -> GraphModel:
|
|||
async with transaction() as tx:
|
||||
await __create_graph(tx, graph, user_id)
|
||||
|
||||
if created_graph := await get_graph(
|
||||
graph.id, graph.version, graph.is_template, user_id=user_id
|
||||
):
|
||||
if created_graph := await get_graph(graph.id, graph.version, user_id=user_id):
|
||||
return created_graph
|
||||
|
||||
raise ValueError(f"Created graph {graph.id} v{graph.version} is not in DB")
|
||||
|
|
|
@ -8,7 +8,7 @@ import threading
|
|||
from concurrent.futures import Future, ProcessPoolExecutor
|
||||
from contextlib import contextmanager
|
||||
from multiprocessing.pool import AsyncResult, Pool
|
||||
from typing import TYPE_CHECKING, Any, Generator, TypeVar, cast
|
||||
from typing import TYPE_CHECKING, Any, Generator, Optional, TypeVar, cast
|
||||
|
||||
from redis.lock import Lock as RedisLock
|
||||
|
||||
|
@ -780,7 +780,8 @@ class ExecutionManager(AppService):
|
|||
graph_id: str,
|
||||
data: BlockInput,
|
||||
user_id: str,
|
||||
graph_version: int | None = None,
|
||||
graph_version: Optional[int] = None,
|
||||
preset_id: str | None = None,
|
||||
) -> GraphExecutionEntry:
|
||||
graph: GraphModel | None = self.db_client.get_graph(
|
||||
graph_id=graph_id, user_id=user_id, version=graph_version
|
||||
|
@ -829,6 +830,7 @@ class ExecutionManager(AppService):
|
|||
graph_version=graph.version,
|
||||
nodes_input=nodes_input,
|
||||
user_id=user_id,
|
||||
preset_id=preset_id,
|
||||
)
|
||||
|
||||
starting_node_execs = []
|
||||
|
|
|
@ -63,7 +63,10 @@ def execute_graph(**kwargs):
|
|||
try:
|
||||
log(f"Executing recurring job for graph #{args.graph_id}")
|
||||
get_execution_client().add_execution(
|
||||
args.graph_id, args.input_data, args.user_id
|
||||
graph_id=args.graph_id,
|
||||
data=args.input_data,
|
||||
user_id=args.user_id,
|
||||
graph_version=args.graph_version,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error executing graph {args.graph_id}: {e}")
|
||||
|
|
|
@ -320,7 +320,8 @@ async def webhook_ingress_generic(
|
|||
continue
|
||||
logger.debug(f"Executing graph #{node.graph_id} node #{node.id}")
|
||||
executor.add_execution(
|
||||
node.graph_id,
|
||||
graph_id=node.graph_id,
|
||||
graph_version=node.graph_version,
|
||||
data={f"webhook_{webhook_id}_payload": payload},
|
||||
user_id=webhook.user_id,
|
||||
)
|
||||
|
|
|
@ -56,3 +56,18 @@ class SetGraphActiveVersion(pydantic.BaseModel):
|
|||
|
||||
class UpdatePermissionsRequest(pydantic.BaseModel):
|
||||
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]
|
||||
)
|
||||
|
|
|
@ -2,6 +2,7 @@ import contextlib
|
|||
import logging
|
||||
import typing
|
||||
|
||||
import autogpt_libs.auth.models
|
||||
import fastapi
|
||||
import fastapi.responses
|
||||
import starlette.middleware.cors
|
||||
|
@ -16,7 +17,10 @@ import backend.data.db
|
|||
import backend.data.graph
|
||||
import backend.data.user
|
||||
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.store.model
|
||||
import backend.server.v2.store.routes
|
||||
import backend.util.service
|
||||
import backend.util.settings
|
||||
|
@ -117,9 +121,27 @@ class AgentServer(backend.util.service.AppProcess):
|
|||
|
||||
@staticmethod
|
||||
async def test_execute_graph(
|
||||
graph_id: str, node_input: dict[typing.Any, typing.Any], user_id: str
|
||||
graph_id: str,
|
||||
graph_version: int,
|
||||
node_input: dict[typing.Any, typing.Any],
|
||||
user_id: str,
|
||||
):
|
||||
return backend.server.routers.v1.execute_graph(graph_id, node_input, user_id)
|
||||
return backend.server.routers.v1.execute_graph(
|
||||
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
|
||||
async def test_create_graph(
|
||||
|
@ -130,7 +152,7 @@ class AgentServer(backend.util.service.AppProcess):
|
|||
|
||||
@staticmethod
|
||||
async def test_get_graph_run_status(graph_exec_id: str, user_id: str):
|
||||
execution = await backend.data.graph.get_execution(
|
||||
execution = await backend.data.graph.get_execution_meta(
|
||||
user_id=user_id, execution_id=graph_exec_id
|
||||
)
|
||||
if not execution:
|
||||
|
@ -138,16 +160,85 @@ class AgentServer(backend.util.service.AppProcess):
|
|||
return execution.status
|
||||
|
||||
@staticmethod
|
||||
async def test_get_graph_run_node_execution_results(
|
||||
async def test_get_graph_run_results(
|
||||
graph_id: str, graph_exec_id: str, user_id: str
|
||||
):
|
||||
return await backend.server.routers.v1.get_graph_run_node_execution_results(
|
||||
return await backend.server.routers.v1.get_graph_execution(
|
||||
graph_id, graph_exec_id, user_id
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
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)
|
||||
|
||||
@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):
|
||||
app.dependency_overrides.update(overrides)
|
||||
|
|
|
@ -13,6 +13,7 @@ from typing_extensions import Optional, TypedDict
|
|||
import backend.data.block
|
||||
import backend.server.integrations.router
|
||||
import backend.server.routers.analytics
|
||||
import backend.server.v2.library.db
|
||||
from backend.data import execution as execution_db
|
||||
from backend.data import graph as graph_db
|
||||
from backend.data.api_key import (
|
||||
|
@ -180,11 +181,6 @@ async def get_graph(
|
|||
tags=["graphs"],
|
||||
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(
|
||||
graph_id: str, user_id: Annotated[str, Depends(get_user_id)]
|
||||
) -> Sequence[graph_db.GraphModel]:
|
||||
|
@ -200,12 +196,11 @@ async def get_graph_all_versions(
|
|||
async def create_new_graph(
|
||||
create_graph: CreateGraph, user_id: Annotated[str, Depends(get_user_id)]
|
||||
) -> graph_db.GraphModel:
|
||||
return await do_create_graph(create_graph, is_template=False, user_id=user_id)
|
||||
return await do_create_graph(create_graph, user_id=user_id)
|
||||
|
||||
|
||||
async def do_create_graph(
|
||||
create_graph: CreateGraph,
|
||||
is_template: bool,
|
||||
# user_id doesn't have to be annotated like on other endpoints,
|
||||
# because create_graph isn't used directly as an endpoint
|
||||
user_id: str,
|
||||
|
@ -217,7 +212,6 @@ async def do_create_graph(
|
|||
graph = await graph_db.get_graph(
|
||||
create_graph.template_id,
|
||||
create_graph.template_version,
|
||||
template=True,
|
||||
user_id=user_id,
|
||||
)
|
||||
if not graph:
|
||||
|
@ -230,11 +224,17 @@ async def do_create_graph(
|
|||
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 = 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,
|
||||
get_credentials=lambda id: integration_creds_manager.get(user_id, id),
|
||||
|
@ -261,11 +261,6 @@ async def delete_graph(
|
|||
@v1_router.put(
|
||||
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(
|
||||
graph_id: str,
|
||||
graph: graph_db.Graph,
|
||||
|
@ -297,6 +292,10 @@ async def update_graph(
|
|||
new_graph_version = await graph_db.create_graph(graph, user_id=user_id)
|
||||
|
||||
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":
|
||||
return integration_creds_manager.get(user_id, credentials_id)
|
||||
|
@ -353,6 +352,12 @@ async def set_graph_active_version(
|
|||
version=new_active_version,
|
||||
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:
|
||||
# Handle deactivation of the previously active version
|
||||
await on_graph_deactivate(
|
||||
|
@ -370,10 +375,11 @@ def execute_graph(
|
|||
graph_id: str,
|
||||
node_input: dict[Any, Any],
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
graph_version: Optional[int] = None,
|
||||
) -> dict[str, Any]: # FIXME: add proper return type
|
||||
try:
|
||||
graph_exec = execution_manager_client().add_execution(
|
||||
graph_id, node_input, user_id=user_id
|
||||
graph_id, node_input, user_id=user_id, graph_version=graph_version
|
||||
)
|
||||
return {"id": graph_exec.graph_exec_id}
|
||||
except Exception as e:
|
||||
|
@ -388,8 +394,10 @@ def execute_graph(
|
|||
)
|
||||
async def stop_graph_run(
|
||||
graph_exec_id: str, user_id: Annotated[str, Depends(get_user_id)]
|
||||
) -> Sequence[execution_db.ExecutionResult]:
|
||||
if not await graph_db.get_execution(user_id=user_id, execution_id=graph_exec_id):
|
||||
) -> graph_db.GraphExecution:
|
||||
if not await graph_db.get_execution_meta(
|
||||
user_id=user_id, execution_id=graph_exec_id
|
||||
):
|
||||
raise HTTPException(404, detail=f"Agent execution #{graph_exec_id} not found")
|
||||
|
||||
await asyncio.to_thread(
|
||||
|
@ -397,7 +405,13 @@ async def stop_graph_run(
|
|||
)
|
||||
|
||||
# Retrieve & return canceled graph execution in its final state
|
||||
return await execution_db.get_execution_results(graph_exec_id)
|
||||
result = await graph_db.get_execution(execution_id=graph_exec_id, user_id=user_id)
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
500,
|
||||
detail=f"Could not fetch graph execution #{graph_exec_id} after stopping",
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@v1_router.get(
|
||||
|
@ -405,10 +419,22 @@ async def stop_graph_run(
|
|||
tags=["graphs"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
async def get_executions(
|
||||
async def get_graphs_executions(
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
) -> list[graph_db.GraphExecution]:
|
||||
return await graph_db.get_executions(user_id=user_id)
|
||||
) -> list[graph_db.GraphExecutionMeta]:
|
||||
return await graph_db.get_graphs_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(
|
||||
|
@ -416,57 +442,20 @@ async def get_executions(
|
|||
tags=["graphs"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
async def get_graph_run_node_execution_results(
|
||||
async def get_graph_execution(
|
||||
graph_id: str,
|
||||
graph_exec_id: str,
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
) -> Sequence[execution_db.ExecutionResult]:
|
||||
) -> graph_db.GraphExecution:
|
||||
graph = await graph_db.get_graph(graph_id, user_id=user_id)
|
||||
if not graph:
|
||||
raise HTTPException(status_code=404, detail=f"Graph #{graph_id} not found.")
|
||||
|
||||
return await execution_db.get_execution_results(graph_exec_id)
|
||||
result = await graph_db.get_execution(execution_id=graph_exec_id, user_id=user_id)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail=f"Graph #{graph_id} not found.")
|
||||
|
||||
|
||||
########################################################
|
||||
##################### 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)
|
||||
return result
|
||||
|
||||
|
||||
########################################################
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import json
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
import prisma.errors
|
||||
import prisma.models
|
||||
|
@ -7,6 +7,7 @@ import prisma.types
|
|||
|
||||
import backend.data.graph
|
||||
import backend.data.includes
|
||||
import backend.server.model
|
||||
import backend.server.v2.library.model
|
||||
import backend.server.v2.store.exceptions
|
||||
|
||||
|
@ -14,90 +15,167 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
async def get_library_agents(
|
||||
user_id: str,
|
||||
) -> List[backend.server.v2.library.model.LibraryAgent]:
|
||||
"""
|
||||
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}")
|
||||
user_id: str, search_query: str | None = None
|
||||
) -> list[backend.server.v2.library.model.LibraryAgent]:
|
||||
logger.debug(
|
||||
f"Fetching library agents for user_id={user_id} search_query={search_query}"
|
||||
)
|
||||
|
||||
try:
|
||||
# Get agents created by user with nodes and links
|
||||
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,
|
||||
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."
|
||||
)
|
||||
|
||||
# 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={
|
||||
where_clause: prisma.types.LibraryAgentWhereInput = {
|
||||
"userId": user_id,
|
||||
"isDeleted": False,
|
||||
"isArchived": False,
|
||||
}
|
||||
|
||||
if search_query:
|
||||
where_clause["OR"] = [
|
||||
{
|
||||
"Agent": {
|
||||
"include": {
|
||||
"AgentNodes": {
|
||||
"include": {
|
||||
"Input": True,
|
||||
"Output": True,
|
||||
"Webhook": True,
|
||||
"AgentBlock": True,
|
||||
}
|
||||
}
|
||||
"is": {"name": {"contains": search_query, "mode": "insensitive"}}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Agent": {
|
||||
"is": {
|
||||
"description": {"contains": search_query, "mode": "insensitive"}
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
try:
|
||||
library_agents = await prisma.models.LibraryAgent.prisma().find_many(
|
||||
where=where_clause,
|
||||
include={
|
||||
"Agent": {
|
||||
"include": {
|
||||
"AgentNodes": {"include": {"Input": True, "Output": True}}
|
||||
}
|
||||
}
|
||||
},
|
||||
order=[{"updatedAt": "desc"}],
|
||||
)
|
||||
logger.debug(f"Retrieved {len(library_agents)} agents for user_id={user_id}.")
|
||||
return [
|
||||
backend.server.v2.library.model.LibraryAgent.from_db(agent)
|
||||
for agent in library_agents
|
||||
]
|
||||
except prisma.errors.PrismaError as e:
|
||||
logger.error(f"Database error fetching library agents: {e}")
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Unable to fetch library agents."
|
||||
)
|
||||
|
||||
# Convert to Graph models first
|
||||
graphs = []
|
||||
|
||||
# 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
|
||||
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 getting library agents: {str(e)}")
|
||||
logger.error(f"Database error creating agent to library: {str(e)}")
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to fetch library agents"
|
||||
"Failed to create agent to library"
|
||||
) from e
|
||||
|
||||
|
||||
async def add_agent_to_library(store_listing_version_id: str, user_id: str) -> None:
|
||||
async def update_agent_version_in_library(
|
||||
user_id: str, agent_id: str, agent_version: int
|
||||
) -> None:
|
||||
"""
|
||||
Finds the agent from the store listing version and adds it to the user's library (UserAgent table)
|
||||
Updates the agent version in the library
|
||||
"""
|
||||
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
|
||||
"""
|
||||
logger.debug(
|
||||
|
@ -131,7 +209,7 @@ async def add_agent_to_library(store_listing_version_id: str, user_id: str) -> N
|
|||
)
|
||||
|
||||
# Check if user already has this agent
|
||||
existing_user_agent = await prisma.models.UserAgent.prisma().find_first(
|
||||
existing_user_agent = await prisma.models.LibraryAgent.prisma().find_first(
|
||||
where={
|
||||
"userId": user_id,
|
||||
"agentId": agent.id,
|
||||
|
@ -145,14 +223,14 @@ async def add_agent_to_library(store_listing_version_id: str, user_id: str) -> N
|
|||
)
|
||||
return
|
||||
|
||||
# Create UserAgent entry
|
||||
await prisma.models.UserAgent.prisma().create(
|
||||
data=prisma.types.UserAgentCreateInput(
|
||||
userId=user_id,
|
||||
agentId=agent.id,
|
||||
agentVersion=agent.version,
|
||||
isCreatedByUser=False,
|
||||
)
|
||||
# Create LibraryAgent entry
|
||||
await prisma.models.LibraryAgent.prisma().create(
|
||||
data={
|
||||
"userId": user_id,
|
||||
"agentId": agent.id,
|
||||
"agentVersion": agent.version,
|
||||
"isCreatedByUser": False,
|
||||
}
|
||||
)
|
||||
logger.debug(f"Added agent {agent.id} to library for user {user_id}")
|
||||
|
||||
|
@ -163,3 +241,127 @@ async def add_agent_to_library(store_listing_version_id: str, user_id: str) -> N
|
|||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to add agent to library"
|
||||
) 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
|
||||
|
|
|
@ -37,7 +37,7 @@ async def test_get_library_agents(mocker):
|
|||
]
|
||||
|
||||
mock_library_agents = [
|
||||
prisma.models.UserAgent(
|
||||
prisma.models.LibraryAgent(
|
||||
id="ua1",
|
||||
userId="test-user",
|
||||
agentId="agent2",
|
||||
|
@ -48,6 +48,7 @@ async def test_get_library_agents(mocker):
|
|||
createdAt=datetime.now(),
|
||||
updatedAt=datetime.now(),
|
||||
isFavorite=False,
|
||||
useGraphIsActiveVersion=True,
|
||||
Agent=prisma.models.AgentGraph(
|
||||
id="agent2",
|
||||
version=1,
|
||||
|
@ -67,8 +68,8 @@ async def test_get_library_agents(mocker):
|
|||
return_value=mock_user_created
|
||||
)
|
||||
|
||||
mock_user_agent = mocker.patch("prisma.models.UserAgent.prisma")
|
||||
mock_user_agent.return_value.find_many = mocker.AsyncMock(
|
||||
mock_library_agent = mocker.patch("prisma.models.LibraryAgent.prisma")
|
||||
mock_library_agent.return_value.find_many = mocker.AsyncMock(
|
||||
return_value=mock_library_agents
|
||||
)
|
||||
|
||||
|
@ -76,40 +77,16 @@ async def test_get_library_agents(mocker):
|
|||
result = await db.get_library_agents("test-user")
|
||||
|
||||
# Verify results
|
||||
assert len(result) == 2
|
||||
assert result[0].id == "agent1"
|
||||
assert result[0].name == "Test Agent 1"
|
||||
assert result[0].description == "Test Description 1"
|
||||
assert result[0].isCreatedByUser is True
|
||||
assert result[1].id == "agent2"
|
||||
assert result[1].name == "Test Agent 2"
|
||||
assert result[1].description == "Test Description 2"
|
||||
assert result[1].isCreatedByUser is False
|
||||
|
||||
# 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0].id == "ua1"
|
||||
assert result[0].name == "Test Agent 2"
|
||||
assert result[0].description == "Test Description 2"
|
||||
assert result[0].is_created_by_user is False
|
||||
assert result[0].is_latest_version is True
|
||||
assert result[0].is_favorite is False
|
||||
assert result[0].agent_id == "agent2"
|
||||
assert result[0].agent_version == 1
|
||||
assert result[0].preset_id is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
@ -152,26 +129,26 @@ async def test_add_agent_to_library(mocker):
|
|||
return_value=mock_store_listing
|
||||
)
|
||||
|
||||
mock_user_agent = mocker.patch("prisma.models.UserAgent.prisma")
|
||||
mock_user_agent.return_value.find_first = mocker.AsyncMock(return_value=None)
|
||||
mock_user_agent.return_value.create = mocker.AsyncMock()
|
||||
mock_library_agent = mocker.patch("prisma.models.LibraryAgent.prisma")
|
||||
mock_library_agent.return_value.find_first = mocker.AsyncMock(return_value=None)
|
||||
mock_library_agent.return_value.create = mocker.AsyncMock()
|
||||
|
||||
# Call function
|
||||
await db.add_agent_to_library("version123", "test-user")
|
||||
await db.add_store_agent_to_library("version123", "test-user")
|
||||
|
||||
# Verify mocks called correctly
|
||||
mock_store_listing_version.return_value.find_unique.assert_called_once_with(
|
||||
where={"id": "version123"}, include={"Agent": True}
|
||||
)
|
||||
mock_user_agent.return_value.find_first.assert_called_once_with(
|
||||
mock_library_agent.return_value.find_first.assert_called_once_with(
|
||||
where={
|
||||
"userId": "test-user",
|
||||
"agentId": "agent1",
|
||||
"agentVersion": 1,
|
||||
}
|
||||
)
|
||||
mock_user_agent.return_value.create.assert_called_once_with(
|
||||
data=prisma.types.UserAgentCreateInput(
|
||||
mock_library_agent.return_value.create.assert_called_once_with(
|
||||
data=prisma.types.LibraryAgentCreateInput(
|
||||
userId="test-user", agentId="agent1", agentVersion=1, isCreatedByUser=False
|
||||
)
|
||||
)
|
||||
|
@ -189,7 +166,7 @@ async def test_add_agent_to_library_not_found(mocker):
|
|||
|
||||
# Call function and verify exception
|
||||
with pytest.raises(backend.server.v2.store.exceptions.AgentNotFoundError):
|
||||
await db.add_agent_to_library("version123", "test-user")
|
||||
await db.add_store_agent_to_library("version123", "test-user")
|
||||
|
||||
# Verify mock called correctly
|
||||
mock_store_listing_version.return_value.find_unique.assert_called_once_with(
|
||||
|
|
|
@ -1,16 +1,112 @@
|
|||
import datetime
|
||||
import json
|
||||
import typing
|
||||
|
||||
import prisma.models
|
||||
import pydantic
|
||||
|
||||
import backend.data.block
|
||||
import backend.data.graph
|
||||
import backend.server.model
|
||||
|
||||
|
||||
class LibraryAgent(pydantic.BaseModel):
|
||||
id: str # Changed from agent_id to match GraphMeta
|
||||
version: int # Changed from agent_version to match GraphMeta
|
||||
is_active: bool # Added to match GraphMeta
|
||||
|
||||
agent_id: str
|
||||
agent_version: int # Changed from agent_version to match GraphMeta
|
||||
|
||||
preset_id: str | None
|
||||
|
||||
updated_at: datetime.datetime
|
||||
|
||||
name: str
|
||||
description: str
|
||||
|
||||
isCreatedByUser: bool
|
||||
# Made input_schema and output_schema match GraphMeta's type
|
||||
input_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
|
||||
|
|
|
@ -1,23 +1,35 @@
|
|||
import datetime
|
||||
|
||||
import prisma.models
|
||||
|
||||
import backend.data.block
|
||||
import backend.server.model
|
||||
import backend.server.v2.library.model
|
||||
|
||||
|
||||
def test_library_agent():
|
||||
agent = backend.server.v2.library.model.LibraryAgent(
|
||||
id="test-agent-123",
|
||||
version=1,
|
||||
is_active=True,
|
||||
agent_id="agent-123",
|
||||
agent_version=1,
|
||||
preset_id=None,
|
||||
updated_at=datetime.datetime.now(),
|
||||
name="Test Agent",
|
||||
description="Test description",
|
||||
isCreatedByUser=False,
|
||||
input_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.version == 1
|
||||
assert agent.is_active is True
|
||||
assert agent.agent_id == "agent-123"
|
||||
assert agent.agent_version == 1
|
||||
assert agent.name == "Test Agent"
|
||||
assert agent.description == "Test description"
|
||||
assert agent.isCreatedByUser is False
|
||||
assert agent.is_favorite 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.output_schema == {"type": "object", "properties": {}}
|
||||
|
||||
|
@ -25,19 +37,148 @@ def test_library_agent():
|
|||
def test_library_agent_with_user_created():
|
||||
agent = backend.server.v2.library.model.LibraryAgent(
|
||||
id="user-agent-456",
|
||||
version=2,
|
||||
is_active=True,
|
||||
agent_id="agent-456",
|
||||
agent_version=2,
|
||||
preset_id=None,
|
||||
updated_at=datetime.datetime.now(),
|
||||
name="User Created Agent",
|
||||
description="An agent created by the user",
|
||||
isCreatedByUser=True,
|
||||
input_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.version == 2
|
||||
assert agent.is_active is True
|
||||
assert agent.agent_id == "agent-456"
|
||||
assert agent.agent_version == 2
|
||||
assert agent.name == "User Created Agent"
|
||||
assert agent.description == "An agent created by the user"
|
||||
assert agent.isCreatedByUser is True
|
||||
assert agent.is_favorite is False
|
||||
assert agent.is_created_by_user is True
|
||||
assert agent.is_latest_version is True
|
||||
assert agent.input_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"}}
|
||||
|
|
|
@ -1,123 +0,0 @@
|
|||
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"
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
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)
|
|
@ -0,0 +1,148 @@
|
|||
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"
|
||||
)
|
|
@ -0,0 +1,156 @@
|
|||
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)
|
|
@ -1,3 +1,5 @@
|
|||
import datetime
|
||||
|
||||
import autogpt_libs.auth.depends
|
||||
import autogpt_libs.auth.middleware
|
||||
import fastapi
|
||||
|
@ -25,9 +27,9 @@ def override_get_user_id():
|
|||
return "test-user-id"
|
||||
|
||||
|
||||
app.dependency_overrides[autogpt_libs.auth.middleware.auth_middleware] = (
|
||||
override_auth_middleware
|
||||
)
|
||||
app.dependency_overrides[
|
||||
autogpt_libs.auth.middleware.auth_middleware
|
||||
] = override_auth_middleware
|
||||
app.dependency_overrides[autogpt_libs.auth.depends.get_user_id] = override_get_user_id
|
||||
|
||||
|
||||
|
@ -35,21 +37,29 @@ def test_get_library_agents_success(mocker: pytest_mock.MockFixture):
|
|||
mocked_value = [
|
||||
backend.server.v2.library.model.LibraryAgent(
|
||||
id="test-agent-1",
|
||||
version=1,
|
||||
is_active=True,
|
||||
agent_id="test-agent-1",
|
||||
agent_version=1,
|
||||
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",
|
||||
description="Test Description 1",
|
||||
isCreatedByUser=True,
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
output_schema={"type": "object", "properties": {}},
|
||||
),
|
||||
backend.server.v2.library.model.LibraryAgent(
|
||||
id="test-agent-2",
|
||||
version=1,
|
||||
is_active=True,
|
||||
agent_id="test-agent-2",
|
||||
agent_version=1,
|
||||
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",
|
||||
description="Test Description 2",
|
||||
isCreatedByUser=False,
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
output_schema={"type": "object", "properties": {}},
|
||||
),
|
||||
|
@ -65,10 +75,10 @@ def test_get_library_agents_success(mocker: pytest_mock.MockFixture):
|
|||
for agent in response.json()
|
||||
]
|
||||
assert len(data) == 2
|
||||
assert data[0].id == "test-agent-1"
|
||||
assert data[0].isCreatedByUser is True
|
||||
assert data[1].id == "test-agent-2"
|
||||
assert data[1].isCreatedByUser is False
|
||||
assert data[0].agent_id == "test-agent-1"
|
||||
assert data[0].is_created_by_user is True
|
||||
assert data[1].agent_id == "test-agent-2"
|
||||
assert data[1].is_created_by_user is False
|
||||
mock_db_call.assert_called_once_with("test-user-id")
|
||||
|
||||
|
||||
|
|
|
@ -325,7 +325,10 @@ async def get_store_submissions(
|
|||
where = prisma.types.StoreSubmissionWhereInput(user_id=user_id)
|
||||
# Query submissions from database
|
||||
submissions = await prisma.models.StoreSubmission.prisma().find_many(
|
||||
where=where, skip=skip, take=page_size, order=[{"date_submitted": "desc"}]
|
||||
where=where,
|
||||
skip=skip,
|
||||
take=page_size,
|
||||
order=[{"date_submitted": "desc"}],
|
||||
)
|
||||
|
||||
# Get total count for pagination
|
||||
|
@ -405,9 +408,7 @@ async def delete_store_submission(
|
|||
)
|
||||
|
||||
# Delete the submission
|
||||
await prisma.models.StoreListing.prisma().delete(
|
||||
where=prisma.types.StoreListingWhereUniqueInput(id=submission.id)
|
||||
)
|
||||
await prisma.models.StoreListing.prisma().delete(where={"id": submission.id})
|
||||
|
||||
logger.debug(
|
||||
f"Successfully deleted submission {submission_id} for user {user_id}"
|
||||
|
@ -504,7 +505,15 @@ async def create_store_submission(
|
|||
"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}")
|
||||
|
@ -521,6 +530,7 @@ async def create_store_submission(
|
|||
status=prisma.enums.SubmissionStatus.PENDING,
|
||||
runs=0,
|
||||
rating=0.0,
|
||||
store_listing_version_id=slv_id,
|
||||
)
|
||||
|
||||
except (
|
||||
|
@ -811,9 +821,7 @@ async def get_agent(
|
|||
|
||||
agent = store_listing_version.Agent
|
||||
|
||||
graph = await backend.data.graph.get_graph(
|
||||
agent.id, agent.version, template=True
|
||||
)
|
||||
graph = await backend.data.graph.get_graph(agent.id, agent.version)
|
||||
|
||||
if not graph:
|
||||
raise fastapi.HTTPException(
|
||||
|
@ -832,3 +840,74 @@ async def get_agent(
|
|||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to fetch agent"
|
||||
) 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
|
||||
|
|
|
@ -115,6 +115,7 @@ class StoreSubmission(pydantic.BaseModel):
|
|||
status: prisma.enums.SubmissionStatus
|
||||
runs: int
|
||||
rating: float
|
||||
store_listing_version_id: str | None = None
|
||||
|
||||
|
||||
class StoreSubmissionsResponse(pydantic.BaseModel):
|
||||
|
@ -151,3 +152,9 @@ class StoreReviewCreate(pydantic.BaseModel):
|
|||
store_listing_version_id: str
|
||||
score: int
|
||||
comments: str | None = None
|
||||
|
||||
|
||||
class ReviewSubmissionRequest(pydantic.BaseModel):
|
||||
store_listing_version_id: str
|
||||
isApproved: bool
|
||||
comments: str
|
||||
|
|
|
@ -642,3 +642,33 @@ async def download_agent_file(
|
|||
return fastapi.responses.FileResponse(
|
||||
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"
|
||||
},
|
||||
)
|
||||
|
|
|
@ -253,7 +253,7 @@ async def block_autogen_agent():
|
|||
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."}
|
||||
response = await server.agent_server.test_execute_graph(
|
||||
test_graph.id, input_data, test_user.id
|
||||
test_graph.id, test_graph.version, input_data, test_user.id
|
||||
)
|
||||
print(response)
|
||||
result = await wait_execution(
|
||||
|
|
|
@ -157,7 +157,7 @@ async def reddit_marketing_agent():
|
|||
test_graph = await create_graph(create_test_graph(), user_id=test_user.id)
|
||||
input_data = {"subreddit": "AutoGPT"}
|
||||
response = await server.agent_server.test_execute_graph(
|
||||
test_graph.id, input_data, test_user.id
|
||||
test_graph.id, test_graph.version, input_data, test_user.id
|
||||
)
|
||||
print(response)
|
||||
result = await wait_execution(test_user.id, test_graph.id, response["id"], 120)
|
||||
|
|
|
@ -8,12 +8,19 @@ from backend.data.user import get_or_create_user
|
|||
from backend.util.test import SpinTestServer, wait_execution
|
||||
|
||||
|
||||
async def create_test_user() -> User:
|
||||
test_user_data = {
|
||||
"sub": "ef3b97d7-1161-4eb4-92b2-10c24fb154c1",
|
||||
"email": "testuser#example.com",
|
||||
"name": "Test User",
|
||||
}
|
||||
async def create_test_user(alt_user: bool = False) -> User:
|
||||
if alt_user:
|
||||
test_user_data = {
|
||||
"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1b",
|
||||
"email": "testuser2#example.com",
|
||||
"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)
|
||||
return user
|
||||
|
||||
|
@ -79,7 +86,7 @@ async def sample_agent():
|
|||
test_graph = await create_graph(create_test_graph(), test_user.id)
|
||||
input_data = {"input_1": "Hello", "input_2": "World"}
|
||||
response = await server.agent_server.test_execute_graph(
|
||||
test_graph.id, input_data, test_user.id
|
||||
test_graph.id, test_graph.version, input_data, test_user.id
|
||||
)
|
||||
print(response)
|
||||
result = await wait_execution(test_user.id, test_graph.id, response["id"], 10)
|
||||
|
|
|
@ -73,9 +73,10 @@ async def wait_execution(
|
|||
# Wait for the executions to complete
|
||||
for i in range(timeout):
|
||||
if await is_execution_completed():
|
||||
return await AgentServer().test_get_graph_run_node_execution_results(
|
||||
graph_exec = await AgentServer().test_get_graph_run_results(
|
||||
graph_id, graph_exec_id, user_id
|
||||
)
|
||||
return graph_exec.node_executions
|
||||
time.sleep(1)
|
||||
|
||||
assert False, "Execution did not complete in time."
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "AgentPreset" ADD COLUMN "isDeleted" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
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;
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "LibraryAgent" ADD COLUMN "useGraphIsActiveVersion" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
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");
|
|
@ -30,7 +30,7 @@ model User {
|
|||
CreditTransaction CreditTransaction[]
|
||||
|
||||
AgentPreset AgentPreset[]
|
||||
UserAgent UserAgent[]
|
||||
LibraryAgent LibraryAgent[]
|
||||
|
||||
Profile Profile[]
|
||||
StoreListing StoreListing[]
|
||||
|
@ -65,7 +65,7 @@ model AgentGraph {
|
|||
AgentGraphExecution AgentGraphExecution[]
|
||||
|
||||
AgentPreset AgentPreset[]
|
||||
UserAgent UserAgent[]
|
||||
LibraryAgent LibraryAgent[]
|
||||
StoreListing StoreListing[]
|
||||
StoreListingVersion StoreListingVersion?
|
||||
|
||||
|
@ -103,15 +103,17 @@ model AgentPreset {
|
|||
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
|
||||
|
||||
InputPresets AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData")
|
||||
UserAgents UserAgent[]
|
||||
LibraryAgents LibraryAgent[]
|
||||
AgentExecution AgentGraphExecution[]
|
||||
|
||||
isDeleted Boolean @default(false)
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
// For the library page
|
||||
// It is a user controlled list of agents, that they will see in there library
|
||||
model UserAgent {
|
||||
model LibraryAgent {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
@ -126,6 +128,8 @@ model UserAgent {
|
|||
agentPresetId String?
|
||||
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
|
||||
|
||||
useGraphIsActiveVersion Boolean @default(false)
|
||||
|
||||
isFavorite Boolean @default(false)
|
||||
isCreatedByUser Boolean @default(false)
|
||||
isArchived Boolean @default(false)
|
||||
|
@ -235,7 +239,7 @@ model AgentGraphExecution {
|
|||
|
||||
AgentNodeExecutions AgentNodeExecution[]
|
||||
|
||||
// Link to User model
|
||||
// Link to User model -- Executed by this user
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
|
@ -443,6 +447,8 @@ view Creator {
|
|||
agent_rating Float
|
||||
agent_runs Int
|
||||
is_featured Boolean
|
||||
|
||||
// Index or unique are not applied to views
|
||||
}
|
||||
|
||||
view StoreAgent {
|
||||
|
@ -465,11 +471,7 @@ view StoreAgent {
|
|||
rating Float
|
||||
versions String[]
|
||||
|
||||
@@unique([creator_username, slug])
|
||||
@@index([creator_username])
|
||||
@@index([featured])
|
||||
@@index([categories])
|
||||
@@index([storeListingVersionId])
|
||||
// Index or unique are not applied to views
|
||||
}
|
||||
|
||||
view StoreSubmission {
|
||||
|
@ -487,7 +489,7 @@ view StoreSubmission {
|
|||
agent_id String
|
||||
agent_version Int
|
||||
|
||||
@@index([user_id])
|
||||
// Index or unique are not applied to views
|
||||
}
|
||||
|
||||
model StoreListing {
|
||||
|
@ -510,9 +512,13 @@ model StoreListing {
|
|||
StoreListingVersions StoreListingVersion[]
|
||||
StoreListingSubmission StoreListingSubmission[]
|
||||
|
||||
@@index([isApproved])
|
||||
@@index([agentId])
|
||||
// Unique index on agentId to ensure only one listing per agent, regardless of number of versions the agent has.
|
||||
@@unique([agentId])
|
||||
@@index([agentId, owningUserId])
|
||||
@@index([owningUserId])
|
||||
// Used in the view query
|
||||
@@index([isDeleted, isApproved])
|
||||
@@index([isDeleted])
|
||||
}
|
||||
|
||||
model StoreListingVersion {
|
||||
|
@ -553,7 +559,7 @@ model StoreListingVersion {
|
|||
StoreListingReview StoreListingReview[]
|
||||
|
||||
@@unique([agentId, agentVersion])
|
||||
@@index([agentId, agentVersion, isApproved])
|
||||
@@index([agentId, agentVersion, isDeleted])
|
||||
}
|
||||
|
||||
model StoreListingReview {
|
||||
|
|
|
@ -2,22 +2,22 @@ import logging
|
|||
|
||||
import pytest
|
||||
|
||||
from backend.util.test import SpinTestServer
|
||||
from backend.util.logging import configure_logging
|
||||
|
||||
# NOTE: You can run tests like with the --log-cli-level=INFO to see the logs
|
||||
# Set up logging
|
||||
configure_logging()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Create console handler with formatting
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
# Reduce Prisma log spam
|
||||
prisma_logger = logging.getLogger("prisma")
|
||||
prisma_logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def server():
|
||||
from backend.util.test import SpinTestServer
|
||||
|
||||
async with SpinTestServer() as server:
|
||||
yield server
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import autogpt_libs.auth.models
|
||||
import fastapi.exceptions
|
||||
import pytest
|
||||
|
||||
import backend.server.v2.store.model
|
||||
from backend.blocks.basic import AgentInputBlock, AgentOutputBlock, StoreValueBlock
|
||||
from backend.data.block import BlockSchema
|
||||
from backend.data.graph import Graph, Link, Node
|
||||
|
@ -202,3 +205,92 @@ async def test_clean_graph(server: SpinTestServer):
|
|||
n for n in created_graph.nodes if n.block_id == AgentInputBlock().id
|
||||
)
|
||||
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
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import logging
|
||||
|
||||
import autogpt_libs.auth.models
|
||||
import fastapi.responses
|
||||
import pytest
|
||||
from prisma.models import User
|
||||
|
||||
from backend.blocks.basic import FindInDictionaryBlock, StoreValueBlock
|
||||
import backend.server.v2.library.model
|
||||
import backend.server.v2.store.model
|
||||
from backend.blocks.basic import AgentInputBlock, FindInDictionaryBlock, StoreValueBlock
|
||||
from backend.blocks.maths import CalculatorBlock, Operation
|
||||
from backend.data import execution, graph
|
||||
from backend.server.model import CreateGraph
|
||||
|
@ -31,7 +35,7 @@ async def execute_graph(
|
|||
|
||||
# --- Test adding new executions --- #
|
||||
response = await agent_server.test_execute_graph(
|
||||
test_graph.id, input_data, test_user.id
|
||||
test_graph.id, test_graph.version, input_data, test_user.id
|
||||
)
|
||||
graph_exec_id = response["id"]
|
||||
logger.info(f"Created execution with ID: {graph_exec_id}")
|
||||
|
@ -51,7 +55,7 @@ async def assert_sample_graph_executions(
|
|||
graph_exec_id: str,
|
||||
):
|
||||
logger.info(f"Checking execution results for graph {test_graph.id}")
|
||||
executions = await agent_server.test_get_graph_run_node_execution_results(
|
||||
graph_run = await agent_server.test_get_graph_run_results(
|
||||
test_graph.id,
|
||||
graph_exec_id,
|
||||
test_user.id,
|
||||
|
@ -70,7 +74,7 @@ async def assert_sample_graph_executions(
|
|||
]
|
||||
|
||||
# Executing StoreValueBlock
|
||||
exec = executions[0]
|
||||
exec = graph_run.node_executions[0]
|
||||
logger.info(f"Checking first StoreValueBlock execution: {exec}")
|
||||
assert exec.status == execution.ExecutionStatus.COMPLETED
|
||||
assert exec.graph_exec_id == graph_exec_id
|
||||
|
@ -83,7 +87,7 @@ async def assert_sample_graph_executions(
|
|||
assert exec.node_id in [test_graph.nodes[0].id, test_graph.nodes[1].id]
|
||||
|
||||
# Executing StoreValueBlock
|
||||
exec = executions[1]
|
||||
exec = graph_run.node_executions[1]
|
||||
logger.info(f"Checking second StoreValueBlock execution: {exec}")
|
||||
assert exec.status == execution.ExecutionStatus.COMPLETED
|
||||
assert exec.graph_exec_id == graph_exec_id
|
||||
|
@ -96,7 +100,7 @@ async def assert_sample_graph_executions(
|
|||
assert exec.node_id in [test_graph.nodes[0].id, test_graph.nodes[1].id]
|
||||
|
||||
# Executing FillTextTemplateBlock
|
||||
exec = executions[2]
|
||||
exec = graph_run.node_executions[2]
|
||||
logger.info(f"Checking FillTextTemplateBlock execution: {exec}")
|
||||
assert exec.status == execution.ExecutionStatus.COMPLETED
|
||||
assert exec.graph_exec_id == graph_exec_id
|
||||
|
@ -111,7 +115,7 @@ async def assert_sample_graph_executions(
|
|||
assert exec.node_id == test_graph.nodes[2].id
|
||||
|
||||
# Executing PrintToConsoleBlock
|
||||
exec = executions[3]
|
||||
exec = graph_run.node_executions[3]
|
||||
logger.info(f"Checking PrintToConsoleBlock execution: {exec}")
|
||||
assert exec.status == execution.ExecutionStatus.COMPLETED
|
||||
assert exec.graph_exec_id == graph_exec_id
|
||||
|
@ -194,14 +198,14 @@ async def test_input_pin_always_waited(server: SpinTestServer):
|
|||
)
|
||||
|
||||
logger.info("Checking execution results")
|
||||
executions = await server.agent_server.test_get_graph_run_node_execution_results(
|
||||
graph_exec = await server.agent_server.test_get_graph_run_results(
|
||||
test_graph.id, graph_exec_id, test_user.id
|
||||
)
|
||||
assert len(executions) == 3
|
||||
assert len(graph_exec.node_executions) == 3
|
||||
# FindInDictionaryBlock should wait for the input pin to be provided,
|
||||
# Hence executing extraction of "key" from {"key1": "value1", "key2": "value2"}
|
||||
assert executions[2].status == execution.ExecutionStatus.COMPLETED
|
||||
assert executions[2].output_data == {"output": ["value2"]}
|
||||
assert graph_exec.node_executions[2].status == execution.ExecutionStatus.COMPLETED
|
||||
assert graph_exec.node_executions[2].output_data == {"output": ["value2"]}
|
||||
logger.info("Completed test_input_pin_always_waited")
|
||||
|
||||
|
||||
|
@ -277,13 +281,265 @@ async def test_static_input_link_on_graph(server: SpinTestServer):
|
|||
server.agent_server, test_graph, test_user, {}, 8
|
||||
)
|
||||
logger.info("Checking execution results")
|
||||
executions = await server.agent_server.test_get_graph_run_node_execution_results(
|
||||
graph_exec = await server.agent_server.test_get_graph_run_results(
|
||||
test_graph.id, graph_exec_id, test_user.id
|
||||
)
|
||||
assert len(executions) == 8
|
||||
assert len(graph_exec.node_executions) == 8
|
||||
# The last 3 executions will be a+b=4+5=9
|
||||
for i, exec_data in enumerate(executions[-3:]):
|
||||
for i, exec_data in enumerate(graph_exec.node_executions[-3:]):
|
||||
logger.info(f"Checking execution {i+1} of last 3: {exec_data}")
|
||||
assert exec_data.status == execution.ExecutionStatus.COMPLETED
|
||||
assert exec_data.output_data == {"result": [9]}
|
||||
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")
|
||||
|
|
|
@ -140,10 +140,10 @@ async def main():
|
|||
print(f"Inserting {NUM_USERS * MAX_AGENTS_PER_USER} user agents")
|
||||
for user in users:
|
||||
num_agents = random.randint(MIN_AGENTS_PER_USER, MAX_AGENTS_PER_USER)
|
||||
for _ in range(num_agents): # Create 1 UserAgent per user
|
||||
for _ in range(num_agents): # Create 1 LibraryAgent per user
|
||||
graph = random.choice(agent_graphs)
|
||||
preset = random.choice(agent_presets)
|
||||
user_agent = await db.useragent.create(
|
||||
user_agent = await db.libraryagent.create(
|
||||
data={
|
||||
"userId": user.id,
|
||||
"agentId": graph.id,
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@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-radio-group": "^1.2.1",
|
||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||
|
@ -42,6 +43,7 @@
|
|||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@sentry/nextjs": "^8",
|
||||
|
|
|
@ -0,0 +1,668 @@
|
|||
"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>
|
||||
);
|
||||
}
|
|
@ -2,54 +2,9 @@
|
|||
@tailwind components;
|
||||
@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 {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--background: 250 5% 98%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
|
@ -61,8 +16,8 @@
|
|||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--accent: 262 83% 58%;
|
||||
--accent-foreground: 0 0% 100%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
|
@ -102,9 +57,7 @@
|
|||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
@ -112,6 +65,14 @@
|
|||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
.font-neue {
|
||||
font-family: "PP Neue Montreal TT", sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
/* *** AutoGPT Design Components *** */
|
||||
|
||||
@layer components {
|
||||
.agpt-border-input {
|
||||
@apply border border-input focus-visible:border-gray-400 focus-visible:outline-none;
|
||||
}
|
||||
|
@ -119,4 +80,67 @@
|
|||
.agpt-shadow-input {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ export default async function RootLayout({
|
|||
},
|
||||
]}
|
||||
/>
|
||||
<main className="flex-1">{children}</main>
|
||||
<main className="w-full flex-1">{children}</main>
|
||||
<TallyPopupSimple />
|
||||
</div>
|
||||
<Toaster />
|
||||
|
|
|
@ -93,59 +93,61 @@ export default function LoginPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<AuthCard>
|
||||
<AuthHeader>Login to your account</AuthHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onLogin)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="m@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel className="flex w-full items-center justify-between">
|
||||
<span>Password</span>
|
||||
<Link
|
||||
href="/reset_password"
|
||||
className="text-sm font-normal leading-normal text-black underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<AuthButton
|
||||
onClick={() => onLogin(form.getValues())}
|
||||
isLoading={isLoading}
|
||||
type="submit"
|
||||
>
|
||||
Login
|
||||
</AuthButton>
|
||||
</form>
|
||||
<AuthFeedback message={feedback} isError={true} />
|
||||
</Form>
|
||||
<AuthBottomText
|
||||
text="Don't have an account?"
|
||||
linkText="Sign up"
|
||||
href="/signup"
|
||||
/>
|
||||
</AuthCard>
|
||||
<div className="flex justify-center">
|
||||
<AuthCard>
|
||||
<AuthHeader>Login to your account</AuthHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onLogin)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="m@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel className="flex w-full items-center justify-between">
|
||||
<span>Password</span>
|
||||
<Link
|
||||
href="/reset_password"
|
||||
className="text-sm font-normal leading-normal text-black underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<AuthButton
|
||||
onClick={() => onLogin(form.getValues())}
|
||||
isLoading={isLoading}
|
||||
type="submit"
|
||||
>
|
||||
Login
|
||||
</AuthButton>
|
||||
</form>
|
||||
<AuthFeedback message={feedback} isError={true} />
|
||||
</Form>
|
||||
<AuthBottomText
|
||||
text="Don't have an account?"
|
||||
linkText="Sign up"
|
||||
href="/signup"
|
||||
/>
|
||||
</AuthCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
"use client";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { GraphExecution, Schedule, GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import {
|
||||
GraphExecutionMeta,
|
||||
Schedule,
|
||||
GraphMeta,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
|
@ -16,10 +20,12 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
|||
|
||||
const Monitor = () => {
|
||||
const [flows, setFlows] = useState<GraphMeta[]>([]);
|
||||
const [executions, setExecutions] = useState<GraphExecution[]>([]);
|
||||
const [executions, setExecutions] = useState<GraphExecutionMeta[]>([]);
|
||||
const [schedules, setSchedules] = useState<Schedule[]>([]);
|
||||
const [selectedFlow, setSelectedFlow] = useState<GraphMeta | null>(null);
|
||||
const [selectedRun, setSelectedRun] = useState<GraphExecution | null>(null);
|
||||
const [selectedRun, setSelectedRun] = useState<GraphExecutionMeta | null>(
|
||||
null,
|
||||
);
|
||||
const [sortColumn, setSortColumn] = useState<keyof Schedule>("id");
|
||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
|
||||
const api = useBackendAPI();
|
||||
|
@ -73,7 +79,7 @@ const Monitor = () => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="grid grid-cols-1 gap-4 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10"
|
||||
className="grid grid-cols-1 gap-4 p-4 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10"
|
||||
data-testid="monitor-page"
|
||||
>
|
||||
<AgentFlowList
|
||||
|
|
|
@ -84,136 +84,138 @@ export default function SignupPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<AuthCard>
|
||||
<AuthHeader>Create a new account</AuthHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSignup)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="m@example.com" {...field} />
|
||||
</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>
|
||||
<div className="flex justify-center">
|
||||
<AuthCard>
|
||||
<AuthHeader>Create a new account</AuthHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSignup)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="m@example.com" {...field} />
|
||||
</FormControl>
|
||||
<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>
|
||||
</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 />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -85,7 +85,6 @@ export default function Page({}: {}) {
|
|||
<PublishAgentPopout
|
||||
trigger={
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
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"
|
||||
|
|
|
@ -92,7 +92,6 @@ export const AgentImageItem: React.FC<AgentImageItemProps> = React.memo(
|
|||
{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]">
|
||||
<Button
|
||||
variant="default"
|
||||
size="default"
|
||||
onClick={() => {
|
||||
if (videoRef.current) {
|
||||
|
|
|
@ -95,7 +95,7 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
|
|||
return (
|
||||
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">
|
||||
{/* Title */}
|
||||
<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">
|
||||
<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">
|
||||
{name}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
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>
|
||||
);
|
||||
}
|
|
@ -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" />
|
||||
|
||||
{/* Title */}
|
||||
<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">
|
||||
<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">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
{/* 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">
|
||||
<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">
|
||||
<h2 className="underline-from-font decoration-skip-ink-none mb-6 text-center font-poppins text-[48px] font-semibold leading-[54px] tracking-[-0.012em] text-neutral-950 dark:text-neutral-50 md:mb-8 lg:mb-12">
|
||||
Build AI agents and share
|
||||
<br />
|
||||
<span className="text-violet-600 dark:text-violet-400">
|
||||
|
@ -51,7 +51,7 @@ export const BecomeACreator: React.FC<BecomeACreatorProps> = ({
|
|||
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"
|
||||
>
|
||||
<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">
|
||||
<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">
|
||||
{buttonText}
|
||||
</span>
|
||||
</button>
|
||||
|
|
|
@ -65,15 +65,12 @@ export const Interactive: Story = {
|
|||
export const Variants: Story = {
|
||||
render: (args) => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button {...args} variant="default">
|
||||
Default
|
||||
<Button {...args} variant="outline">
|
||||
Outline (default)
|
||||
</Button>
|
||||
<Button {...args} variant="destructive">
|
||||
Destructive
|
||||
</Button>
|
||||
<Button {...args} variant="outline">
|
||||
Outline
|
||||
</Button>
|
||||
<Button {...args} variant="secondary">
|
||||
Secondary
|
||||
</Button>
|
||||
|
|
|
@ -5,16 +5,15 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-[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",
|
||||
"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",
|
||||
{
|
||||
variants: {
|
||||
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:
|
||||
"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:
|
||||
"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",
|
||||
"border border-black/50 text-[#272727] hover:bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700",
|
||||
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",
|
||||
ghost:
|
||||
|
@ -22,17 +21,17 @@ const buttonVariants = cva(
|
|||
link: "text-[#272727] underline-offset-4 hover:underline dark:text-neutral-100",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"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",
|
||||
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",
|
||||
default: "h-10 px-4 py-2 rounded-full text-sm",
|
||||
sm: "h-8 px-3 py-1.5 rounded-full text-xs",
|
||||
lg: "h-12 px-5 py-2.5 rounded-full text-lg",
|
||||
primary:
|
||||
"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 sm:h-12 sm:w-12 md:h-14 md:w-14 lg:h-[4.375rem] lg:w-[4.375rem]",
|
||||
"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]",
|
||||
icon: "h-10 w-10",
|
||||
card: "h-12 p-5 agpt-rounded-card justify-center text-lg",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
variant: "outline",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
|
@ -43,13 +42,13 @@ export interface ButtonProps
|
|||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
variant?:
|
||||
| "default"
|
||||
| "destructive"
|
||||
| "accent"
|
||||
| "outline"
|
||||
| "secondary"
|
||||
| "ghost"
|
||||
| "link";
|
||||
size?: "default" | "sm" | "lg" | "primary" | "icon";
|
||||
size?: "default" | "sm" | "lg" | "primary" | "icon" | "card";
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
|
|
|
@ -33,7 +33,7 @@ export const CreatorInfoCard: React.FC<CreatorInfoCardProps> = ({
|
|||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex w-full flex-col items-start justify-start gap-1.5">
|
||||
<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">
|
||||
<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">
|
||||
{username}
|
||||
</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">
|
||||
|
|
|
@ -34,7 +34,7 @@ export const FeaturedStoreCard: React.FC<FeaturedStoreCardProps> = ({
|
|||
data-testid="featured-store-card"
|
||||
>
|
||||
<div className="flex h-[188px] flex-col items-start justify-start gap-3 self-stretch">
|
||||
<h2 className="font-poppins self-stretch text-[35px] font-medium leading-10 text-neutral-900 dark:text-neutral-100">
|
||||
<h2 className="self-stretch font-poppins text-[35px] font-medium leading-10 text-neutral-900 dark:text-neutral-100">
|
||||
{agentName}
|
||||
</h2>
|
||||
<div className="font-lead self-stretch text-xl font-normal leading-7 text-neutral-800 dark:text-neutral-200">
|
||||
|
|
|
@ -86,7 +86,6 @@ export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
|
|||
) : (
|
||||
<Link href="/login">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex items-center justify-end space-x-2"
|
||||
>
|
||||
|
@ -133,11 +132,7 @@ export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
|
|||
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"
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Button size="sm" className="flex items-center space-x-2">
|
||||
<IconLogIn className="h-5 w-5" />
|
||||
<span>Log In</span>
|
||||
</Button>
|
||||
|
|
|
@ -49,7 +49,7 @@ export const NavbarLink = ({ name, href }: NavbarLinkProps) => {
|
|||
/>
|
||||
)}
|
||||
<div
|
||||
className={`font-poppins text-[20px] font-medium leading-[28px] ${
|
||||
className={`hidden font-poppins text-[20px] font-medium leading-[28px] lg:block ${
|
||||
activeLink === href
|
||||
? "text-neutral-50 dark:text-neutral-900"
|
||||
: "text-neutral-900 dark:text-neutral-50"
|
||||
|
|
|
@ -256,7 +256,6 @@ export const ProfileInfoForm = ({ profile }: { profile: CreatorDetails }) => {
|
|||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="default"
|
||||
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"
|
||||
onClick={submitForm}
|
||||
|
|
|
@ -100,14 +100,12 @@ 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">
|
||||
<Button
|
||||
onClick={onDone}
|
||||
variant="outline"
|
||||
className="h-12 w-full rounded-[59px] sm:flex-1"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
View progress
|
||||
|
|
|
@ -84,7 +84,6 @@ export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
|
|||
</div>
|
||||
<Button
|
||||
onClick={onOpenBuilder}
|
||||
variant="default"
|
||||
size="lg"
|
||||
className="bg-neutral-800 text-white hover:bg-neutral-900"
|
||||
>
|
||||
|
@ -152,7 +151,6 @@ 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">
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="w-full sm:flex-1"
|
||||
>
|
||||
|
@ -165,7 +163,6 @@ export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
|
|||
}
|
||||
}}
|
||||
disabled={!selectedAgentId || !selectedAgentVersion}
|
||||
variant="default"
|
||||
size="default"
|
||||
className="w-full bg-neutral-800 text-white hover:bg-neutral-900 sm:flex-1"
|
||||
>
|
||||
|
|
|
@ -342,7 +342,6 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
|
|||
You can use AI to generate a cover image for you
|
||||
</p>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
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" : ""
|
||||
|
@ -424,7 +423,6 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
|
|||
<div className="flex justify-between gap-4 border-t border-slate-200 p-6 dark:border-slate-700">
|
||||
<Button
|
||||
onClick={onBack}
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="w-full dark:border-slate-700 dark:text-slate-300 sm:flex-1"
|
||||
>
|
||||
|
@ -432,7 +430,6 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
|
|||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
variant="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"
|
||||
>
|
||||
|
|
|
@ -71,7 +71,7 @@ export const StoreCard: React.FC<StoreCardProps> = ({
|
|||
{/* Content Section */}
|
||||
<div className="w-full px-2 py-4">
|
||||
{/* Title and Creator */}
|
||||
<h3 className="font-poppins mb-2 text-2xl font-semibold leading-tight text-[#272727] dark:text-neutral-100">
|
||||
<h3 className="mb-2 font-poppins text-2xl font-semibold leading-tight text-[#272727] dark:text-neutral-100">
|
||||
{agentName}
|
||||
</h3>
|
||||
{!hideAvatar && creatorName && (
|
||||
|
|
|
@ -46,7 +46,7 @@ export const AgentsSection: React.FC<AgentsSectionProps> = ({
|
|||
return (
|
||||
<div className="flex flex-col items-center justify-center py-4 lg:py-8">
|
||||
<div className="w-full max-w-[1360px]">
|
||||
<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">
|
||||
<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">
|
||||
{sectionTitle}
|
||||
</div>
|
||||
{!displayedAgents || displayedAgents.length === 0 ? (
|
||||
|
@ -64,7 +64,7 @@ export const AgentsSection: React.FC<AgentsSectionProps> = ({
|
|||
>
|
||||
<CarouselContent>
|
||||
{displayedAgents.map((agent, index) => (
|
||||
<CarouselItem key={index} className="min-w-64 max-w-68">
|
||||
<CarouselItem key={index} className="min-w-64 max-w-71">
|
||||
<StoreCard
|
||||
agentName={agent.agent_name}
|
||||
agentImage={agent.agent_image}
|
||||
|
|
|
@ -33,7 +33,7 @@ export const FeaturedCreators: React.FC<FeaturedCreatorsProps> = ({
|
|||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center py-16">
|
||||
<div className="w-full max-w-[1360px]">
|
||||
<h2 className="font-poppins mb-8 text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
|
||||
<h2 className="mb-8 font-poppins text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
|
|||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center">
|
||||
<div className="w-[99vw]">
|
||||
<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">
|
||||
<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">
|
||||
Featured agents
|
||||
</h2>
|
||||
|
||||
|
|
|
@ -281,7 +281,7 @@ export const PublishAgentPopout: React.FC<PublishAgentPopoutProps> = ({
|
|||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
{trigger || <Button variant="default">Publish Agent</Button>}
|
||||
{trigger || <Button>Publish Agent</Button>}
|
||||
</PopoverTrigger>
|
||||
<PopoverAnchor asChild>
|
||||
<div className="fixed left-0 top-0 hidden h-screen w-screen items-center justify-center"></div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import BackendAPI, {
|
||||
GraphExecution,
|
||||
GraphExecutionMeta,
|
||||
GraphMeta,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import React, { useMemo } from "react";
|
||||
|
@ -40,7 +40,7 @@ export const AgentFlowList = ({
|
|||
className,
|
||||
}: {
|
||||
flows: GraphMeta[];
|
||||
executions?: GraphExecution[];
|
||||
executions?: GraphExecutionMeta[];
|
||||
selectedFlow: GraphMeta | null;
|
||||
onSelectFlow: (f: GraphMeta) => void;
|
||||
className?: string;
|
||||
|
@ -109,7 +109,7 @@ export const AgentFlowList = ({
|
|||
{flows
|
||||
.map((flow) => {
|
||||
let runCount = 0,
|
||||
lastRun: GraphExecution | null = null;
|
||||
lastRun: GraphExecutionMeta | null = null;
|
||||
if (executions) {
|
||||
const _flowRuns = executions.filter(
|
||||
(r) => r.graph_id == flow.id,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import {
|
||||
GraphExecution,
|
||||
GraphExecutionMeta,
|
||||
Graph,
|
||||
GraphMeta,
|
||||
safeCopyGraph,
|
||||
|
@ -32,7 +32,6 @@ import {
|
|||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { CronScheduler } from "@/components/cronScheduler";
|
||||
import RunnerInputUI from "@/components/runner-ui/RunnerInputUI";
|
||||
import useAgentGraph from "@/hooks/useAgentGraph";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
|
@ -40,7 +39,7 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
|||
export const FlowInfo: React.FC<
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
flow: GraphMeta;
|
||||
executions: GraphExecution[];
|
||||
executions: GraphExecutionMeta[];
|
||||
flowVersion?: number | "all";
|
||||
refresh: () => void;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
GraphExecution,
|
||||
GraphExecutionMeta,
|
||||
GraphMeta,
|
||||
NodeExecutionResult,
|
||||
SpecialBlockID,
|
||||
|
@ -18,7 +18,7 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
|||
export const FlowRunInfo: React.FC<
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
flow: GraphMeta;
|
||||
execution: GraphExecution;
|
||||
execution: GraphExecutionMeta;
|
||||
}
|
||||
> = ({ flow, execution, ...props }) => {
|
||||
const [isOutputOpen, setIsOutputOpen] = useState(false);
|
||||
|
@ -26,10 +26,9 @@ export const FlowRunInfo: React.FC<
|
|||
const api = useBackendAPI();
|
||||
|
||||
const fetchBlockResults = useCallback(async () => {
|
||||
const executionResults = await api.getGraphExecutionInfo(
|
||||
flow.id,
|
||||
execution.execution_id,
|
||||
);
|
||||
const executionResults = (
|
||||
await api.getGraphExecutionInfo(flow.id, execution.execution_id)
|
||||
).node_executions;
|
||||
|
||||
// Create a map of the latest COMPLETED execution results of output nodes by node_id
|
||||
const latestCompletedResults = executionResults
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { GraphExecution } from "@/lib/autogpt-server-api";
|
||||
import { GraphExecutionMeta } from "@/lib/autogpt-server-api";
|
||||
|
||||
export const FlowRunStatusBadge: React.FC<{
|
||||
status: GraphExecution["status"];
|
||||
status: GraphExecutionMeta["status"];
|
||||
className?: string;
|
||||
}> = ({ status, className }) => (
|
||||
<Badge
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { GraphExecution, GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import { GraphExecutionMeta, GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Table,
|
||||
|
@ -15,10 +15,10 @@ import { TextRenderer } from "../ui/render";
|
|||
|
||||
export const FlowRunsList: React.FC<{
|
||||
flows: GraphMeta[];
|
||||
executions: GraphExecution[];
|
||||
executions: GraphExecutionMeta[];
|
||||
className?: string;
|
||||
selectedRun?: GraphExecution | null;
|
||||
onSelectRun: (r: GraphExecution) => void;
|
||||
selectedRun?: GraphExecutionMeta | null;
|
||||
onSelectRun: (r: GraphExecutionMeta) => void;
|
||||
}> = ({ flows, executions, selectedRun, onSelectRun, className }) => (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from "react";
|
||||
import { GraphExecution, GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import { GraphExecutionMeta, GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import { CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
@ -12,7 +12,7 @@ import { FlowRunsTimeline } from "@/components/monitor/FlowRunsTimeline";
|
|||
|
||||
export const FlowRunsStatus: React.FC<{
|
||||
flows: GraphMeta[];
|
||||
executions: GraphExecution[];
|
||||
executions: GraphExecutionMeta[];
|
||||
title?: string;
|
||||
className?: string;
|
||||
}> = ({ flows, executions: executions, title, className }) => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { GraphExecution, GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import { GraphExecutionMeta, GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import {
|
||||
ComposedChart,
|
||||
DefaultLegendContentProps,
|
||||
|
@ -23,7 +23,7 @@ export const FlowRunsTimeline = ({
|
|||
className,
|
||||
}: {
|
||||
flows: GraphMeta[];
|
||||
executions: GraphExecution[];
|
||||
executions: GraphExecutionMeta[];
|
||||
dataMin: "dataMin" | number;
|
||||
className?: string;
|
||||
}) => (
|
||||
|
@ -60,8 +60,10 @@ export const FlowRunsTimeline = ({
|
|||
<Tooltip
|
||||
content={({ payload, label }) => {
|
||||
if (payload && payload.length) {
|
||||
const data: GraphExecution & { time: number; _duration: number } =
|
||||
payload[0].payload;
|
||||
const data: GraphExecutionMeta & {
|
||||
time: number;
|
||||
_duration: number;
|
||||
} = payload[0].payload;
|
||||
const flow = flows.find((f) => f.id === data.graph_id);
|
||||
return (
|
||||
<Card className="p-2 text-xs leading-normal">
|
||||
|
|
|
@ -11,7 +11,7 @@ const badgeVariants = cva(
|
|||
default:
|
||||
"border-transparent bg-neutral-900 text-neutral-50 shadow dark:bg-neutral-50 dark:text-neutral-900",
|
||||
secondary:
|
||||
"border-transparent bg-neutral-100 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-50",
|
||||
"border-transparent bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-50",
|
||||
destructive:
|
||||
"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",
|
||||
|
|
|
@ -9,7 +9,7 @@ const Card = React.forwardRef<
|
|||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border border-gray-300 bg-white text-neutral-950 shadow",
|
||||
"agpt-card text-neutral-950", // TODO: check styling of existing usages
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
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,
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
"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 };
|
|
@ -8,6 +8,7 @@ import {
|
|||
CredentialsDeleteResponse,
|
||||
CredentialsMetaResponse,
|
||||
GraphExecution,
|
||||
GraphExecutionMeta,
|
||||
Graph,
|
||||
GraphCreatable,
|
||||
GraphExecuteResponse,
|
||||
|
@ -96,10 +97,6 @@ export default class BackendAPI {
|
|||
return this._get(`/graphs`);
|
||||
}
|
||||
|
||||
getExecutions(): Promise<GraphExecution[]> {
|
||||
return this._get(`/executions`);
|
||||
}
|
||||
|
||||
getGraph(
|
||||
id: string,
|
||||
version?: number,
|
||||
|
@ -148,22 +145,37 @@ export default class BackendAPI {
|
|||
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(
|
||||
graphID: string,
|
||||
runID: string,
|
||||
): Promise<NodeExecutionResult[]> {
|
||||
return (await this._get(`/graphs/${graphID}/executions/${runID}`)).map(
|
||||
): Promise<GraphExecution> {
|
||||
const result = await this._get(`/graphs/${graphID}/executions/${runID}`);
|
||||
result.node_executions = result.node_executions.map(
|
||||
parseNodeExecutionResultTimestamps,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
async stopGraphExecution(
|
||||
graphID: string,
|
||||
runID: string,
|
||||
): Promise<NodeExecutionResult[]> {
|
||||
return (
|
||||
await this._request("POST", `/graphs/${graphID}/executions/${runID}/stop`)
|
||||
).map(parseNodeExecutionResultTimestamps);
|
||||
): Promise<GraphExecution> {
|
||||
const result = await this._request(
|
||||
"POST",
|
||||
`/graphs/${graphID}/executions/${runID}/stop`,
|
||||
);
|
||||
result.node_executions = result.node_executions.map(
|
||||
parseNodeExecutionResultTimestamps,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
oAuthLogin(
|
||||
|
@ -535,7 +547,22 @@ export default class BackendAPI {
|
|||
let errorDetail;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorDetail = errorData.detail || response.statusText;
|
||||
if (
|
||||
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) {
|
||||
errorDetail = response.statusText;
|
||||
}
|
||||
|
@ -717,6 +744,13 @@ type WebsocketMessage = {
|
|||
};
|
||||
}[keyof WebsocketMessageTypeMap];
|
||||
|
||||
type _PydanticValidationError = {
|
||||
type: string;
|
||||
loc: string[];
|
||||
msg: string;
|
||||
input: any;
|
||||
};
|
||||
|
||||
/* *** HELPER FUNCTIONS *** */
|
||||
|
||||
function parseNodeExecutionResultTimestamps(result: any): NodeExecutionResult {
|
||||
|
|
|
@ -41,6 +41,8 @@ export type BlockIOSubSchema =
|
|||
| BlockIOSimpleTypeSubSchema
|
||||
| BlockIOCombinedTypeSubSchema;
|
||||
|
||||
export type BlockIOSubType = BlockIOSimpleTypeSubSchema["type"];
|
||||
|
||||
export type BlockIOSimpleTypeSubSchema =
|
||||
| BlockIOObjectSubSchema
|
||||
| BlockIOCredentialsSubSchema
|
||||
|
@ -133,7 +135,7 @@ export const PROVIDER_NAMES = {
|
|||
export type CredentialsProviderName =
|
||||
(typeof PROVIDER_NAMES)[keyof typeof PROVIDER_NAMES];
|
||||
|
||||
export type BlockIOCredentialsSubSchema = BlockIOSubSchemaMeta & {
|
||||
export type BlockIOCredentialsSubSchema = BlockIOObjectSubSchema & {
|
||||
/* Mirror of backend/data/model.py:CredentialsFieldSchemaExtra */
|
||||
credentials_provider: CredentialsProviderName[];
|
||||
credentials_scopes?: string[];
|
||||
|
@ -191,8 +193,8 @@ export type LinkCreatable = Omit<Link, "id" | "is_static"> & {
|
|||
id?: string;
|
||||
};
|
||||
|
||||
/* Mirror of backend/data/graph.py:GraphExecution */
|
||||
export type GraphExecution = {
|
||||
/* Mirror of backend/data/graph.py:GraphExecutionMeta */
|
||||
export type GraphExecutionMeta = {
|
||||
execution_id: string;
|
||||
started_at: number;
|
||||
ended_at: number;
|
||||
|
@ -201,6 +203,14 @@ export type GraphExecution = {
|
|||
status: "QUEUED" | "RUNNING" | "COMPLETED" | "TERMINATED" | "FAILED";
|
||||
graph_id: string;
|
||||
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 = {
|
||||
|
|
|
@ -42,39 +42,75 @@ test.describe("Build", () => { //(1)!
|
|||
});
|
||||
// --8<-- [end:BuildPageExample]
|
||||
|
||||
test("user can add all blocks", async ({ page }, testInfo) => {
|
||||
test("user can add all blocks a-l", async ({ page }, testInfo) => {
|
||||
// this test is slow af so we 10x the timeout (sorry future me)
|
||||
await test.setTimeout(testInfo.timeout * 10);
|
||||
await test.setTimeout(testInfo.timeout * 100);
|
||||
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();
|
||||
|
||||
// add all the blocks in order
|
||||
const blocksToSkip = await buildPage.getBlocksToSkip();
|
||||
|
||||
// add all the blocks in order except for the agent executor block
|
||||
for (const block of blocks) {
|
||||
if (block.id !== "e189baac-8c20-45a1-94a7-55177ea42565") {
|
||||
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.id !== "e189baac-8c20-45a1-94a7-55177ea42565") {
|
||||
if (block.name[0].toLowerCase() >= "m") {
|
||||
continue;
|
||||
}
|
||||
if (!blocksToSkip.some((b) => b === block.id)) {
|
||||
console.log("Checking block:", block.name);
|
||||
await test.expect(buildPage.hasBlock(block)).resolves.toBeTruthy();
|
||||
}
|
||||
}
|
||||
// fill in the input for the agent input block
|
||||
await buildPage.fillBlockInputByPlaceholder(
|
||||
blocks.find((b) => b.name === "Agent Input")?.id ?? "",
|
||||
"Enter Name",
|
||||
"Agent Input Field",
|
||||
);
|
||||
await buildPage.fillBlockInputByPlaceholder(
|
||||
blocks.find((b) => b.name === "Agent Output")?.id ?? "",
|
||||
"Enter Name",
|
||||
"Agent Output Field",
|
||||
);
|
||||
|
||||
// check that we can save the agent with all the blocks
|
||||
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
|
||||
await test.expect(page).toHaveURL(new RegExp("/.*build\\?flowID=.+"));
|
||||
});
|
||||
|
||||
test("user can add all blocks m-z", async ({ page }, testInfo) => {
|
||||
// this test is slow af so we 10x the timeout (sorry future me)
|
||||
await test.setTimeout(testInfo.timeout * 100);
|
||||
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
|
||||
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
|
||||
|
|
|
@ -6,8 +6,7 @@ import { v4 as uuidv4 } from "uuid";
|
|||
import * as fs from "fs/promises";
|
||||
import path from "path";
|
||||
// --8<-- [start:AttachAgentId]
|
||||
|
||||
test.describe.skip("Monitor", () => {
|
||||
test.describe("Monitor", () => {
|
||||
let buildPage: BuildPage;
|
||||
let monitorPage: MonitorPage;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ElementHandle, Locator, Page } from "@playwright/test";
|
||||
import { BasePage } from "./base.page";
|
||||
|
||||
interface Block {
|
||||
export interface Block {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
|
@ -378,6 +378,39 @@ 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> {
|
||||
console.log(`clicking next tutorial step`);
|
||||
await this.page.getByRole("button", { name: "Next" }).click();
|
||||
|
@ -448,6 +481,15 @@ 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> {
|
||||
console.log(`waiting for run tutorial button`);
|
||||
await this.page.waitForSelector('[id="press-run-label"]');
|
||||
|
|
|
@ -43,9 +43,6 @@ export class MonitorPage extends BasePage {
|
|||
async isLoaded(): Promise<boolean> {
|
||||
console.log(`checking if monitor page is loaded`);
|
||||
try {
|
||||
// Wait for network to settle first
|
||||
await this.page.waitForLoadState("networkidle", { timeout: 10_000 });
|
||||
|
||||
// Wait for the monitor page
|
||||
await this.page.getByTestId("monitor-page").waitFor({
|
||||
state: "visible",
|
||||
|
@ -55,7 +52,7 @@ export class MonitorPage extends BasePage {
|
|||
// Wait for table headers to be visible (indicates table structure is ready)
|
||||
await this.page.locator("thead th").first().waitFor({
|
||||
state: "visible",
|
||||
timeout: 5_000,
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
// Wait for either a table row or an empty tbody to be present
|
||||
|
@ -63,14 +60,14 @@ export class MonitorPage extends BasePage {
|
|||
// Wait for at least one row
|
||||
this.page.locator("tbody tr[data-testid]").first().waitFor({
|
||||
state: "visible",
|
||||
timeout: 5_000,
|
||||
timeout: 15_000,
|
||||
}),
|
||||
// OR wait for an empty tbody (indicating no agents but table is loaded)
|
||||
this.page
|
||||
.locator("tbody[data-testid='agent-flow-list-body']:empty")
|
||||
.waitFor({
|
||||
state: "visible",
|
||||
timeout: 5_000,
|
||||
timeout: 15_000,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ const config = {
|
|||
mono: ["var(--font-geist-mono)"],
|
||||
// Include the custom font family
|
||||
neue: ['"PP Neue Montreal TT"', "sans-serif"],
|
||||
poppin: ["var(--font-poppins)"],
|
||||
poppins: ["var(--font-poppins)"],
|
||||
inter: ["var(--font-inter)"],
|
||||
},
|
||||
colors: {
|
||||
|
@ -95,26 +95,20 @@ const config = {
|
|||
28: "7rem",
|
||||
32: "8rem",
|
||||
36: "9rem",
|
||||
39: "9.875rem",
|
||||
40: "10rem",
|
||||
44: "11rem",
|
||||
48: "12rem",
|
||||
52: "13rem",
|
||||
56: "14rem",
|
||||
69: "14.875rem",
|
||||
60: "15rem",
|
||||
63: "15.875rem",
|
||||
64: "16rem",
|
||||
68: "17.75rem",
|
||||
68: "17rem",
|
||||
70: "17.5rem",
|
||||
71: "17.75rem",
|
||||
72: "18rem",
|
||||
77: "18.5625rem",
|
||||
76: "19rem",
|
||||
80: "20rem",
|
||||
89: "22.5rem",
|
||||
96: "24rem",
|
||||
110: "27.5rem",
|
||||
139: "37.1875rem",
|
||||
167: "41.6875rem",
|
||||
225: "56.25rem",
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
|
|
|
@ -2342,6 +2342,26 @@
|
|||
aria-hidden "^1.1.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":
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.4.tgz#d83104e5fb588870a673b55f3387da4844e5836e"
|
||||
|
@ -2502,6 +2522,20 @@
|
|||
"@radix-ui/react-use-previous" "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":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.2.4.tgz#52fe0e5f169209b7fa300673491a6bedde940279"
|
||||
|
|
Loading…
Reference in New Issue