Compare commits

...

18 Commits

Author SHA1 Message Date
Reinier van der Leer
f032333a28 Merge branch 'dev' into pwuts/open-2330-implement-agent-presets-functionality 2025-04-10 23:11:15 +02:00
Reinier van der Leer
773e4af6d4 add references to backend on frontend types 2025-04-10 15:05:31 +02:00
Reinier van der Leer
a18ca17f25 feat(backend): Allow creating preset directly from graph execution 2025-04-10 15:04:21 +02:00
Reinier van der Leer
383b72a551 Merge branch 'dev' into pwuts/open-2330-implement-agent-presets-functionality 2025-04-10 14:28:01 +02:00
Reinier van der Leer
158ce75c38 remove redundant set_is_deleted_for_library_agent 2025-04-10 14:25:01 +02:00
Reinier van der Leer
cd35e215e3 Merge branch 'dev' into pwuts/secrt-1215-agent-presets-back-end 2025-04-10 14:24:45 +02:00
Reinier van der Leer
fba9ef2bd7 add view for agent presets 2025-04-09 02:16:50 +02:00
Reinier van der Leer
32cf7fae8e fix agent presets API endpoints 2025-04-09 02:15:59 +02:00
Reinier van der Leer
4eab94510a fix presets pagination 2025-04-09 02:11:24 +02:00
Reinier van der Leer
a25d12ba26 Add Presets ("Template runs") to Runs list on /library/agents/[id] 2025-04-08 20:04:14 +02:00
Reinier van der Leer
e94d0844bb Merge branch 'dev' into pwuts/open-2330-implement-agent-presets-functionality 2025-04-08 11:33:54 +02:00
Reinier van der Leer
7045ff190f fix migration 2025-04-08 11:18:15 +02:00
Reinier van der Leer
b78d0787f5 fix type errors 2025-04-07 18:58:00 +02:00
Reinier van der Leer
6476bcbca2 Merge branch 'dev' into pwuts/open-2330-implement-agent-presets-functionality 2025-04-07 16:54:54 +02:00
Reinier van der Leer
e908213049 WIP: feat(frontend): Agent Presets UI (+ needed changes to backend endpoints)
Frontend:
- Add logic to fetch, select/view, and create presets to `/library/agents/[id]`
- refactor: Brand `LibraryAgentPreset.id` + references

Backend:
- Add `graph_id` filter parameter to `GET /library/presets` (`list_presets`) endpoint
2025-02-28 13:19:58 +01:00
Reinier van der Leer
57a7bfe2b7 update DB schema 2025-02-27 17:56:53 +01:00
Reinier van der Leer
612461fe22 brand GraphExecution.execution_id and Schedule.id 2025-02-26 17:06:22 +01:00
Reinier van der Leer
e982fe99ac implement agent run "Delete" option 2025-02-26 16:58:04 +01:00
15 changed files with 556 additions and 250 deletions

View File

@@ -201,7 +201,7 @@ class AgentServer(backend.util.service.AppProcess):
@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(
return await backend.server.v2.library.routes.presets.list_presets(
user_id=user_id, page=page, page_size=page_size
)
@@ -213,7 +213,7 @@ class AgentServer(backend.util.service.AppProcess):
@staticmethod
async def test_create_preset(
preset: backend.server.v2.library.model.CreateLibraryAgentPresetRequest,
preset: backend.server.v2.library.model.LibraryAgentPresetCreatable,
user_id: str,
):
return await backend.server.v2.library.routes.presets.create_preset(
@@ -223,7 +223,7 @@ class AgentServer(backend.util.service.AppProcess):
@staticmethod
async def test_update_preset(
preset_id: str,
preset: backend.server.v2.library.model.CreateLibraryAgentPresetRequest,
preset: backend.server.v2.library.model.LibraryAgentPresetUpdatable,
user_id: str,
):
return await backend.server.v2.library.routes.presets.update_preset(

View File

@@ -14,7 +14,9 @@ import backend.server.v2.store.exceptions as store_exceptions
import backend.server.v2.store.image_gen as store_image_gen
import backend.server.v2.store.media as store_media
from backend.data.db import locked_transaction
from backend.data.execution import get_graph_execution
from backend.data.includes import library_agent_include
from backend.util.exceptions import NotFoundError
from backend.util.settings import Config
logger = logging.getLogger(__name__)
@@ -140,7 +142,7 @@ async def get_library_agent(id: str, user_id: str) -> library_model.LibraryAgent
Get a specific agent from the user's library.
Args:
library_agent_id: ID of the library agent to retrieve.
id: ID of the library agent to retrieve.
user_id: ID of the authenticated user.
Returns:
@@ -232,7 +234,7 @@ async def create_library_agent(
isCreatedByUser=(user_id == graph.user_id),
useGraphIsActiveVersion=True,
User={"connect": {"id": user_id}},
# Creator={"connect": {"id": agent.userId}},
# Creator={"connect": {"id": graph.user_id}},
AgentGraph={
"connect": {
"graphVersionId": {"id": graph.id, "version": graph.version}
@@ -398,11 +400,13 @@ async def add_store_agent_to_library(
# Check if user already has this agent
existing_library_agent = (
await prisma.models.LibraryAgent.prisma().find_first(
await prisma.models.LibraryAgent.prisma().find_unique(
where={
"userId": user_id,
"agentGraphId": graph.id,
"agentGraphVersion": graph.version,
"userId_agentGraphId_agentGraphVersion": {
"userId": user_id,
"agentGraphId": graph.id,
"agentGraphVersion": graph.version,
}
},
include=library_agent_include(user_id),
)
@@ -410,13 +414,13 @@ async def add_store_agent_to_library(
if existing_library_agent:
if existing_library_agent.isDeleted:
# Even if agent exists it needs to be marked as not deleted
await set_is_deleted_for_library_agent(
user_id, graph.id, graph.version, False
await update_library_agent(
existing_library_agent.id, user_id, is_deleted=False
)
else:
logger.debug(
f"User #{user_id} already has graph #{graph.id} "
"in their library"
f"v{graph.version} in their library"
)
return library_model.LibraryAgent.from_db(existing_library_agent)
@@ -424,8 +428,11 @@ async def add_store_agent_to_library(
added_agent = await prisma.models.LibraryAgent.prisma().create(
data=prisma.types.LibraryAgentCreateInput(
userId=user_id,
agentGraphId=graph.id,
agentGraphVersion=graph.version,
AgentGraph={
"connect": {
"graphVersionId": {"id": graph.id, "version": graph.version}
}
},
isCreatedByUser=False,
),
include=library_agent_include(user_id),
@@ -445,60 +452,22 @@ async def add_store_agent_to_library(
raise store_exceptions.DatabaseError("Failed to add agent to library") from e
async def set_is_deleted_for_library_agent(
user_id: str, agent_id: str, agent_version: int, is_deleted: bool
) -> None:
"""
Changes the isDeleted flag for a library agent.
Args:
user_id: The user's library from which the agent is being removed.
agent_id: The ID of the agent to remove.
agent_version: The version of the agent to remove.
is_deleted: Whether the agent is being marked as deleted.
Raises:
DatabaseError: If there's an issue updating the Library
"""
logger.debug(
f"Setting isDeleted={is_deleted} for agent {agent_id} v{agent_version} "
f"in library for user {user_id}"
)
try:
logger.warning(
f"Setting isDeleted={is_deleted} for agent {agent_id} v{agent_version} in library for user {user_id}"
)
count = await prisma.models.LibraryAgent.prisma().update_many(
where={
"userId": user_id,
"agentGraphId": agent_id,
"agentGraphVersion": agent_version,
},
data={"isDeleted": is_deleted},
)
logger.warning(f"Updated {count} isDeleted library agents")
except prisma.errors.PrismaError as e:
logger.error(f"Database error setting agent isDeleted: {e}")
raise store_exceptions.DatabaseError(
"Failed to set agent isDeleted in library"
) from e
##############################################
########### Presets DB Functions #############
##############################################
async def get_presets(
user_id: str, page: int, page_size: int
async def list_presets(
user_id: str, page: int, page_size: int, graph_id: Optional[str] = None
) -> library_model.LibraryAgentPresetResponse:
"""
Retrieves a paginated list of AgentPresets for the specified user.
Args:
user_id: The user ID whose presets are being retrieved.
page: The current page index (0-based or 1-based, clarify in your domain).
page: The current page index (1-based).
page_size: Number of items to retrieve per page.
graph_id: Agent Graph ID to filter by.
Returns:
A LibraryAgentPresetResponse containing a list of presets and pagination info.
@@ -510,21 +479,24 @@ async def get_presets(
f"Fetching presets for user #{user_id}, page={page}, page_size={page_size}"
)
if page < 0 or page_size < 1:
if page < 1 or page_size < 1:
logger.warning(
"Invalid pagination input: page=%d, page_size=%d", page, page_size
)
raise store_exceptions.DatabaseError("Invalid pagination parameters")
query_filter: prisma.types.AgentPresetWhereInput = {"userId": user_id}
if graph_id:
query_filter["agentGraphId"] = graph_id
try:
presets_records = await prisma.models.AgentPreset.prisma().find_many(
where={"userId": user_id},
skip=page * page_size,
where=query_filter,
skip=(page - 1) * page_size,
take=page_size,
include={"InputPresets": True},
)
total_items = await prisma.models.AgentPreset.prisma().count(
where={"userId": user_id}
)
total_items = await prisma.models.AgentPreset.prisma().count(where=query_filter)
total_pages = (total_items + page_size - 1) // page_size
presets = [
@@ -577,69 +549,142 @@ async def get_preset(
raise store_exceptions.DatabaseError("Failed to fetch preset") from e
async def upsert_preset(
async def create_preset(
user_id: str,
preset: library_model.CreateLibraryAgentPresetRequest,
preset_id: Optional[str] = None,
preset: library_model.LibraryAgentPresetCreatable,
) -> library_model.LibraryAgentPreset:
"""
Creates or updates an AgentPreset for a user.
Creates a new AgentPreset for a user.
Args:
user_id: The ID of the user creating/updating the preset.
preset: The preset data used for creation or update.
preset_id: An optional preset ID to update; if None, a new preset is created.
user_id: The ID of the user creating the preset.
preset: The preset data used for creation.
Returns:
The newly created or updated LibraryAgentPreset.
The newly created LibraryAgentPreset.
Raises:
DatabaseError: If there's a database error in creating or updating the preset.
DatabaseError: If there's a database error in creating the preset.
"""
logger.debug(
f"Creating preset ({repr(preset.name)}) for user #{user_id}",
)
try:
new_preset = await prisma.models.AgentPreset.prisma().create(
data=prisma.types.AgentPresetCreateInput(
userId=user_id,
name=preset.name,
description=preset.description,
agentGraphId=preset.graph_id,
agentGraphVersion=preset.graph_version,
isActive=preset.is_active,
InputPresets={
"create": [
prisma.types.AgentNodeExecutionInputOutputCreateWithoutRelationsInput( # noqa
name=name, data=prisma.fields.Json(data)
)
for name, data in preset.inputs.items()
]
},
),
include={"InputPresets": True},
)
return library_model.LibraryAgentPreset.from_db(new_preset)
except prisma.errors.PrismaError as e:
logger.error(f"Database error creating preset: {e}")
raise store_exceptions.DatabaseError("Failed to create preset") from e
async def create_preset_from_graph_execution(
user_id: str,
create_request: library_model.LibraryAgentPresetCreatableFromGraphExecution,
) -> library_model.LibraryAgentPreset:
"""
Creates a new AgentPreset from an AgentGraphExecution.
Params:
user_id: The ID of the user creating the preset.
create_request: The data used for creation.
Returns:
The newly created LibraryAgentPreset.
Raises:
DatabaseError: If there's a database error in creating the preset.
"""
graph_exec_id = create_request.graph_execution_id
graph_execution = await get_graph_execution(user_id, graph_exec_id)
if not graph_execution:
raise NotFoundError(f"Graph execution #{graph_exec_id} not found")
logger.debug(
f"Creating preset for user #{user_id} from graph execution #{graph_exec_id}",
)
return await create_preset(
user_id=user_id,
preset=library_model.LibraryAgentPresetCreatable(
inputs=graph_execution.inputs,
graph_id=graph_execution.graph_id,
graph_version=graph_execution.graph_version,
name=create_request.name,
description=create_request.description,
is_active=create_request.is_active,
),
)
async def update_preset(
user_id: str,
preset_id: str,
preset: library_model.LibraryAgentPresetUpdatable,
) -> library_model.LibraryAgentPreset:
"""
Updates an existing AgentPreset for a user.
Args:
user_id: The ID of the user updating the preset.
preset_id: The ID of the preset to update.
preset: The preset data used for the update.
Returns:
The updated LibraryAgentPreset.
Raises:
DatabaseError: If there's a database error in updating the preset.
ValueError: If attempting to update a non-existent preset.
"""
logger.debug(
f"Upserting preset #{preset_id} ({repr(preset.name)}) for user #{user_id}",
f"Updating preset #{preset_id} ({repr(preset.name)}) for user #{user_id}",
)
try:
inputs = [
prisma.types.AgentNodeExecutionInputOutputCreateWithoutRelationsInput(
name=name, data=prisma.fields.Json(data)
)
for name, data in preset.inputs.items()
]
if preset_id:
# Update existing preset
updated = await prisma.models.AgentPreset.prisma().update(
where={"id": preset_id},
data={
"name": preset.name,
"description": preset.description,
"isActive": preset.is_active,
"InputPresets": {"create": inputs},
},
include={"InputPresets": True},
)
if not updated:
raise ValueError(f"AgentPreset #{preset_id} not found")
return library_model.LibraryAgentPreset.from_db(updated)
else:
# Create new preset
new_preset = await prisma.models.AgentPreset.prisma().create(
data=prisma.types.AgentPresetCreateInput(
userId=user_id,
name=preset.name,
description=preset.description,
agentGraphId=preset.graph_id,
agentGraphVersion=preset.graph_version,
isActive=preset.is_active,
InputPresets={"create": inputs},
),
include={"InputPresets": True},
)
return library_model.LibraryAgentPreset.from_db(new_preset)
update_data: prisma.types.AgentPresetUpdateInput = {}
if preset.name:
update_data["name"] = preset.name
if preset.description:
update_data["description"] = preset.description
if preset.inputs:
update_data["InputPresets"] = {
"create": [
prisma.types.AgentNodeExecutionInputOutputCreateWithoutRelationsInput( # noqa
name=name, data=prisma.fields.Json(data)
)
for name, data in preset.inputs.items()
]
}
if preset.is_active:
update_data["isActive"] = preset.is_active
updated = await prisma.models.AgentPreset.prisma().update(
where={"id": preset_id},
data=update_data,
include={"InputPresets": True},
)
if not updated:
raise ValueError(f"AgentPreset #{preset_id} not found")
return library_model.LibraryAgentPreset.from_db(updated)
except prisma.errors.PrismaError as e:
logger.error(f"Database error upserting preset: {e}")
raise store_exceptions.DatabaseError("Failed to create preset") from e
logger.error(f"Database error updating preset: {e}")
raise store_exceptions.DatabaseError("Failed to update preset") from e
async def delete_preset(user_id: str, preset_id: str) -> None:

View File

@@ -168,27 +168,62 @@ class LibraryAgentResponse(pydantic.BaseModel):
pagination: server_model.Pagination
class LibraryAgentPreset(pydantic.BaseModel):
class LibraryAgentPresetCreatable(pydantic.BaseModel):
"""
Request model used when creating a new preset for a library agent.
"""
graph_id: str
graph_version: int
inputs: block_model.BlockInput
name: str
description: str
is_active: bool = True
class LibraryAgentPresetCreatableFromGraphExecution(pydantic.BaseModel):
"""
Request model used when creating a new preset for a library agent.
"""
graph_execution_id: str
name: str
description: str
is_active: bool = True
class LibraryAgentPresetUpdatable(pydantic.BaseModel):
"""
Request model used when updating a preset for a library agent.
"""
inputs: Optional[block_model.BlockInput] = None
name: Optional[str] = None
description: Optional[str] = None
is_active: Optional[bool] = None
class LibraryAgentPreset(LibraryAgentPresetCreatable):
"""Represents a preset configuration for a library agent."""
id: str
updated_at: datetime.datetime
graph_id: str
graph_version: int
name: str
description: str
is_active: bool
inputs: block_model.BlockInput
@classmethod
def from_db(cls, preset: prisma.models.AgentPreset) -> "LibraryAgentPreset":
if preset.InputPresets is None:
raise ValueError("Input values must be included in object")
input_data: block_model.BlockInput = {}
for preset_input in preset.InputPresets or []:
for preset_input in preset.InputPresets:
input_data[preset_input.name] = preset_input.data
return cls(
@@ -210,19 +245,6 @@ class LibraryAgentPresetResponse(pydantic.BaseModel):
pagination: server_model.Pagination
class CreateLibraryAgentPresetRequest(pydantic.BaseModel):
"""
Request model used when creating a new preset for a library agent.
"""
name: str
description: str
inputs: block_model.BlockInput
graph_id: str
graph_version: int
is_active: bool
class LibraryAgentFilter(str, Enum):
"""Possible filters for searching library agents."""

View File

@@ -1,14 +1,15 @@
import logging
from typing import Annotated, Any
from typing import Annotated, Any, Optional
import autogpt_libs.auth as autogpt_auth_lib
import autogpt_libs.utils.cache
from fastapi import APIRouter, Body, Depends, HTTPException, status
from fastapi import APIRouter, Body, Depends, HTTPException, Query, status
import backend.executor
import backend.server.v2.library.db as db
import backend.server.v2.library.model as models
import backend.util.service
from backend.util.exceptions import NotFoundError
logger = logging.getLogger(__name__)
@@ -26,10 +27,13 @@ def execution_manager_client() -> backend.executor.ExecutionManager:
summary="List presets",
description="Retrieve a paginated list of presets for the current user.",
)
async def get_presets(
async def list_presets(
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
page: int = 1,
page_size: int = 10,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=10, ge=1),
graph_id: Optional[str] = Query(
description="Allows to filter presets by a specific agent graph"
),
) -> models.LibraryAgentPresetResponse:
"""
Retrieve a paginated list of presets for the current user.
@@ -38,12 +42,18 @@ async def get_presets(
user_id (str): ID of the authenticated user.
page (int): Page number for pagination.
page_size (int): Number of items per page.
graph_id: Allows to filter presets by a specific agent graph.
Returns:
models.LibraryAgentPresetResponse: A response containing the list of presets.
"""
try:
return await db.get_presets(user_id, page, page_size)
return await db.list_presets(
user_id=user_id,
graph_id=graph_id,
page=page,
page_size=page_size,
)
except Exception as e:
logger.exception(f"Exception occurred while getting presets: {e}")
raise HTTPException(
@@ -96,14 +106,17 @@ async def get_preset(
description="Create a new preset for the current user.",
)
async def create_preset(
preset: models.CreateLibraryAgentPresetRequest,
preset: (
models.LibraryAgentPresetCreatable
| models.LibraryAgentPresetCreatableFromGraphExecution
),
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
) -> models.LibraryAgentPreset:
"""
Create a new library agent preset. Automatically corrects node_input format if needed.
Args:
preset (models.CreateLibraryAgentPresetRequest): The preset data to create.
preset (models.LibraryAgentPresetCreatable): The preset data to create.
user_id (str): ID of the authenticated user.
Returns:
@@ -113,7 +126,12 @@ async def create_preset(
HTTPException: If an error occurs while creating the preset.
"""
try:
return await db.upsert_preset(user_id, preset)
if isinstance(preset, models.LibraryAgentPresetCreatable):
return await db.create_preset(user_id, preset)
else:
return await db.create_preset_from_graph_execution(user_id, preset)
except NotFoundError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
except Exception as e:
logger.exception(f"Exception occurred while creating preset: {e}")
raise HTTPException(
@@ -122,22 +140,22 @@ async def create_preset(
)
@router.put(
@router.patch(
"/presets/{preset_id}",
summary="Update an existing preset",
description="Update an existing preset by its ID.",
)
async def update_preset(
preset_id: str,
preset: models.CreateLibraryAgentPresetRequest,
preset: models.LibraryAgentPresetUpdatable,
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
) -> models.LibraryAgentPreset:
"""
Update an existing library agent preset. If the preset doesn't exist, it may be created.
Update an existing library agent preset.
Args:
preset_id (str): ID of the preset to update.
preset (models.CreateLibraryAgentPresetRequest): The preset data to update.
preset (models.LibraryAgentPresetUpdatable): The preset data to update.
user_id (str): ID of the authenticated user.
Returns:
@@ -147,7 +165,9 @@ async def update_preset(
HTTPException: If an error occurs while updating the preset.
"""
try:
return await db.upsert_preset(user_id, preset, preset_id)
return await db.update_preset(
user_id=user_id, preset_id=preset_id, preset=preset
)
except Exception as e:
logger.exception(f"Exception occurred whilst updating preset: {e}")
raise HTTPException(

View File

@@ -357,7 +357,7 @@ async def test_execute_preset(server: SpinTestServer):
test_graph = await create_graph(server, test_graph, test_user)
# Create preset with initial values
preset = backend.server.v2.library.model.CreateLibraryAgentPresetRequest(
preset = backend.server.v2.library.model.LibraryAgentPresetCreatable(
name="Test Preset With Clash",
description="Test preset with clashing input values",
graph_id=test_graph.id,
@@ -446,7 +446,7 @@ async def test_execute_preset_with_clash(server: SpinTestServer):
test_graph = await create_graph(server, test_graph, test_user)
# Create preset with initial values
preset = backend.server.v2.library.model.CreateLibraryAgentPresetRequest(
preset = backend.server.v2.library.model.LibraryAgentPresetCreatable(
name="Test Preset With Clash",
description="Test preset with clashing input values",
graph_id=test_graph.id,

View File

@@ -10,16 +10,12 @@ from prisma.types import (
AgentGraphCreateInput,
AgentNodeCreateInput,
AgentNodeLinkCreateInput,
AgentPresetCreateInput,
AnalyticsDetailsCreateInput,
AnalyticsMetricsCreateInput,
APIKeyCreateInput,
CreditTransactionCreateInput,
LibraryAgentCreateInput,
ProfileCreateInput,
StoreListingCreateInput,
StoreListingReviewCreateInput,
StoreListingVersionCreateInput,
UserCreateInput,
)

View File

@@ -12,6 +12,8 @@ import {
GraphID,
LibraryAgent,
LibraryAgentID,
LibraryAgentPreset,
LibraryAgentPresetID,
Schedule,
ScheduleID,
} from "@/lib/autogpt-server-api";
@@ -23,6 +25,20 @@ import AgentRunDetailsView from "@/components/agents/agent-run-details-view";
import AgentRunsSelectorList from "@/components/agents/agent-runs-selector-list";
import AgentScheduleDetailsView from "@/components/agents/agent-schedule-details-view";
export type AgentRunsViewSelection =
| {
type: "run";
id?: GraphExecutionID;
}
| {
type: "preset";
id: LibraryAgentPresetID;
}
| {
type: "schedule";
id: ScheduleID;
};
export default function AgentRunsPage(): React.ReactElement {
const { id: agentID }: { id: LibraryAgentID } = useParams();
const router = useRouter();
@@ -33,11 +49,11 @@ export default function AgentRunsPage(): React.ReactElement {
const [graph, setGraph] = useState<Graph | null>(null);
const [agent, setAgent] = useState<LibraryAgent | null>(null);
const [agentRuns, setAgentRuns] = useState<GraphExecutionMeta[]>([]);
const [agentPresets, setAgentPresets] = useState<LibraryAgentPreset[]>([]);
const [schedules, setSchedules] = useState<Schedule[]>([]);
const [selectedView, selectView] = useState<
| { type: "run"; id?: GraphExecutionID }
| { type: "schedule"; id: ScheduleID }
>({ type: "run" });
const [selectedView, selectView] = useState<AgentRunsViewSelection>({
type: "run",
});
const [selectedRun, setSelectedRun] = useState<
GraphExecution | GraphExecutionMeta | null
>(null);
@@ -58,6 +74,10 @@ export default function AgentRunsPage(): React.ReactElement {
selectView({ type: "run", id });
}, []);
const selectPreset = useCallback((id: LibraryAgentPresetID) => {
selectView({ type: "preset", id });
}, []);
const selectSchedule = useCallback((schedule: Schedule) => {
selectView({ type: "schedule", id: schedule.id });
setSelectedSchedule(schedule);
@@ -151,6 +171,20 @@ export default function AgentRunsPage(): React.ReactElement {
};
}, [api, selectedView.id]);
const fetchPresets = useCallback(async () => {
if (!agent) return;
await api
.listLibraryAgentPresets({ graph_id: agent.graph_id })
.then(
(response) =>
setAgentPresets(response.presets) /* TODO: handle pagination */,
);
}, [api, agent]);
useEffect(() => {
fetchPresets();
}, [fetchPresets]);
// load selectedRun based on selectedView
useEffect(() => {
if (selectedView.type != "run" || !selectedView.id || !agent) return;
@@ -221,6 +255,19 @@ export default function AgentRunsPage(): React.ReactElement {
[api, agent],
);
const createPresetFromRun = useCallback(
async (run: GraphExecutionMeta) => {
const createdPreset = await api.createLibraryAgentPreset({
/* FIXME: add dialog to enter name and description */
name: agent!.name,
description: agent!.description,
graph_execution_id: run.id,
});
setAgentPresets((prev) => [...prev, createdPreset]);
},
[agent, api],
);
const agentActions: ButtonAction[] = useMemo(
() => [
...(agent?.can_access_graph
@@ -254,14 +301,18 @@ export default function AgentRunsPage(): React.ReactElement {
className="agpt-div w-full border-b lg:w-auto lg:border-b-0 lg:border-r"
agent={agent}
agentRuns={agentRuns}
agentPresets={agentPresets}
schedules={schedules}
selectedView={selectedView}
allowDraftNewRun={!graph.has_webhook_trigger}
onSelectRun={selectRun}
onSelectPreset={selectPreset}
onSelectSchedule={selectSchedule}
onSelectDraftNewRun={openRunDraftView}
onPinAsPreset={createPresetFromRun}
onDeleteRun={setConfirmingDeleteAgentRun}
onDeleteSchedule={(id) => deleteSchedule(id)}
// TODO: onDeletePreset={deletePreset}
onDeleteSchedule={deleteSchedule}
/>
<div className="flex-1">
@@ -290,6 +341,25 @@ export default function AgentRunsPage(): React.ReactElement {
<AgentRunDraftView
graph={graph}
onRun={(runID) => selectRun(runID)}
onSavePreset={(preset) => {
setAgentPresets((prev) => [...prev, preset]);
selectPreset(preset.id);
}}
agentActions={agentActions}
/>
) : selectedView.type == "preset" ? (
<AgentRunDraftView
graph={graph}
preset={agentPresets.find((ap) => ap.id == selectedView.id)!}
onRun={(runID) => selectRun(runID)}
onSavePreset={(preset) =>
setAgentPresets((prev) =>
prev.with(
agentPresets.findIndex((ap) => ap.id == preset.id),
preset,
),
)
}
agentActions={agentActions}
/>
) : selectedView.type == "schedule" ? (

View File

@@ -1,8 +1,12 @@
"use client";
import React, { useCallback, useMemo, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { GraphExecutionID, GraphMeta } from "@/lib/autogpt-server-api";
import {
GraphExecutionID,
GraphMeta,
LibraryAgentPreset,
} from "@/lib/autogpt-server-api";
import type { ButtonAction } from "@/components/agptui/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -11,14 +15,19 @@ import { useToastOnFail } from "@/components/ui/use-toast";
import ActionButtonGroup from "@/components/agptui/action-button-group";
import SchemaTooltip from "@/components/SchemaTooltip";
import { IconPlay } from "@/components/ui/icons";
import { deepEquals } from "@/lib/utils";
export default function AgentRunDraftView({
graph,
preset,
onRun,
onSavePreset,
agentActions,
}: {
graph: GraphMeta;
preset?: LibraryAgentPreset;
onRun: (runID: GraphExecutionID) => void;
onSavePreset: (preset: LibraryAgentPreset) => void;
agentActions: ButtonAction[];
}): React.ReactNode {
const api = useBackendAPI();
@@ -27,6 +36,17 @@ export default function AgentRunDraftView({
const agentInputs = graph.input_schema.properties;
const [inputValues, setInputValues] = useState<Record<string, any>>({});
useEffect(() => {
if (preset == undefined) {
setInputValues({});
return;
}
if (deepEquals(preset.inputs, inputValues)) return;
setInputValues(preset.inputs);
// Sets `inputValues` once if `preset` is changed.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [preset]);
const doRun = useCallback(
() =>
api
@@ -36,6 +56,34 @@ export default function AgentRunDraftView({
[api, graph, inputValues, onRun, toastOnFail],
);
const savePreset = useCallback(
() =>
(preset
? // Update existing preset
api.updateLibraryAgentPreset(preset.id, {
...preset, // TODO: update specific attributes
inputs: inputValues,
})
: // Save run draft as new preset
api.createLibraryAgentPreset({
graph_id: graph.id,
graph_version: graph.version,
name: graph.name,
description: "", // TODO: add dialog for name + description
inputs: inputValues,
})
).then(onSavePreset),
[
preset,
api,
inputValues,
graph.id,
graph.version,
graph.name,
onSavePreset,
],
);
const runActions: ButtonAction[] = useMemo(
() => [
{
@@ -48,8 +96,12 @@ export default function AgentRunDraftView({
variant: "accent",
callback: doRun,
},
{
label: preset ? "Save preset" : "Save as a preset",
callback: savePreset,
},
],
[doRun],
[doRun, preset, savePreset],
);
return (

View File

@@ -1,5 +1,6 @@
import React from "react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { GraphExecutionMeta } from "@/lib/autogpt-server-api/types";
@@ -22,7 +23,6 @@ export const agentRunStatusMap: Record<
QUEUED: "queued",
RUNNING: "running",
TERMINATED: "stopped",
// TODO: implement "draft" - https://github.com/Significant-Gravitas/AutoGPT/issues/9168
};
const statusData: Record<
@@ -51,13 +51,19 @@ const statusStyles = {
export default function AgentRunStatusChip({
status,
className,
}: {
status: AgentRunStatus;
className?: string;
}): React.ReactElement {
return (
<Badge
variant="secondary"
className={`text-xs font-medium ${statusStyles[statusData[status].variant]} rounded-[45px] px-[9px] py-[3px]`}
className={cn(
"rounded-[45px] px-[9px] py-[3px] text-xs font-medium",
statusStyles[statusData[status].variant],
className,
)}
>
{statusData[status].label}
</Badge>

View File

@@ -1,6 +1,6 @@
import React from "react";
import moment from "moment";
import { MoreVertical } from "lucide-react";
import { MoreVertical, PinIcon } from "lucide-react";
import { cn } from "@/lib/utils";
@@ -17,23 +17,28 @@ import AgentRunStatusChip, {
AgentRunStatus,
} from "@/components/agents/agent-run-status-chip";
export type AgentRunSummaryProps = {
status: AgentRunStatus;
export type AgentRunSummaryProps = (
| { type: "run"; status: AgentRunStatus }
| { type: "preset" | "schedule"; status?: undefined }
) & {
title: string;
timestamp: number | Date;
timestamp?: number | Date;
selected?: boolean;
onClick?: () => void;
onPinAsPreset?: () => void;
// onRename: () => void;
onDelete: () => void;
onDelete?: () => void;
className?: string;
};
export default function AgentRunSummaryCard({
type,
status,
title,
timestamp,
selected = false,
onClick,
onPinAsPreset,
// onRename,
onDelete,
className,
@@ -48,9 +53,17 @@ export default function AgentRunSummaryCard({
onClick={onClick}
>
<CardContent className="relative p-2.5 lg:p-4">
<AgentRunStatusChip status={status} />
{type == "run" ? (
<AgentRunStatusChip status={status} />
) : type == "schedule" ? (
<AgentRunStatusChip status="scheduled" />
) : (
<div className="flex items-center text-sm text-neutral-700">
<PinIcon className="mr-2 size-4" /> Template run
</div>
)}
<div className="mt-5 flex items-center justify-between">
<div className="mt-3 flex items-center justify-between">
<h3 className="truncate pr-2 text-base font-medium text-neutral-900">
{title}
</h3>
@@ -62,25 +75,30 @@ export default function AgentRunSummaryCard({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{/* {onPinAsPreset && (
{onPinAsPreset && (
<DropdownMenuItem onClick={onPinAsPreset}>
Pin as a preset
</DropdownMenuItem>
)} */}
</DropdownMenuItem>
)}
{/* <DropdownMenuItem onClick={onRename}>Rename</DropdownMenuItem> */}
<DropdownMenuItem onClick={onDelete}>Delete</DropdownMenuItem>
{onDelete && (
<DropdownMenuItem onClick={onDelete}>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>
{timestamp && (
<p
className="mt-1 text-sm font-normal text-neutral-500"
title={moment(timestamp).toString()}
>
{{ run: "Ran", schedule: "Next run", preset: "Last updated" }[type]}{" "}
{moment(timestamp).fromNow()}
</p>
)}
</CardContent>
</Card>
);

View File

@@ -7,6 +7,8 @@ import {
GraphExecutionID,
GraphExecutionMeta,
LibraryAgent,
LibraryAgentPreset,
LibraryAgentPresetID,
Schedule,
ScheduleID,
} from "@/lib/autogpt-server-api";
@@ -15,34 +17,43 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/agptui/Button";
import { Badge } from "@/components/ui/badge";
import { AgentRunsViewSelection } from "@/app/library/agents/[id]/page";
import { agentRunStatusMap } from "@/components/agents/agent-run-status-chip";
import AgentRunSummaryCard from "@/components/agents/agent-run-summary-card";
interface AgentRunsSelectorListProps {
agent: LibraryAgent;
agentRuns: GraphExecutionMeta[];
agentPresets: LibraryAgentPreset[];
schedules: Schedule[];
selectedView: { type: "run" | "schedule"; id?: string };
selectedView: AgentRunsViewSelection;
allowDraftNewRun?: boolean;
onSelectRun: (id: GraphExecutionID) => void;
onSelectPreset: (id: LibraryAgentPresetID) => void;
onSelectSchedule: (schedule: Schedule) => void;
onSelectDraftNewRun: () => void;
onDeleteRun: (id: GraphExecutionMeta) => void;
// onDeletePreset: (id: LibraryAgentPresetID) => void;
onDeleteSchedule: (id: ScheduleID) => void;
onPinAsPreset: (run: GraphExecutionMeta) => void;
className?: string;
}
export default function AgentRunsSelectorList({
agent,
agentRuns,
agentPresets,
schedules,
selectedView,
allowDraftNewRun = true,
onSelectRun,
onSelectPreset,
onSelectSchedule,
onSelectDraftNewRun,
onDeleteRun,
// onDeletePreset,
onDeleteSchedule,
onPinAsPreset,
className,
}: AgentRunsSelectorListProps): React.ReactElement {
const [activeListTab, setActiveListTab] = useState<"runs" | "scheduled">(
@@ -89,7 +100,7 @@ export default function AgentRunsSelectorList({
</Badge>
</div>
{/* Runs / Schedules list */}
{/* Runs+Presets / Schedules list */}
<ScrollArea className="lg:h-[calc(100vh-200px)]">
<div className="flex gap-2 lg:flex-col">
{/* New Run button - only in small layouts */}
@@ -109,37 +120,61 @@ export default function AgentRunsSelectorList({
</Button>
)}
{activeListTab === "runs"
? agentRuns
{activeListTab === "runs" ? (
<>
{/* Presets */}
{agentPresets
.toSorted(
(a, b) => b.updated_at.getTime() - a.updated_at.getTime(),
)
.map((preset) => (
<AgentRunSummaryCard
className="h-28 w-72 lg:h-auto xl:w-80"
key={preset.id}
type="preset"
title={preset.name}
selected={selectedView.id === preset.id}
onClick={() => onSelectPreset(preset.id)}
// onDelete={() => onDeletePreset(preset.id)}
/>
))}
{/* Runs */}
{agentRuns
.toSorted(
(a, b) => b.started_at.getTime() - a.started_at.getTime(),
)
.map((run) => (
<AgentRunSummaryCard
className="h-28 w-72 lg:h-32 xl:w-80"
className="h-28 w-72 lg:h-30 xl:w-80"
key={run.id}
type="run"
status={agentRunStatusMap[run.status]}
title={agent.name}
timestamp={run.started_at}
selected={selectedView.id === run.id}
onClick={() => onSelectRun(run.id)}
onDelete={() => onDeleteRun(run)}
/>
))
: schedules
.filter((schedule) => schedule.graph_id === agent.graph_id)
.map((schedule) => (
<AgentRunSummaryCard
className="h-28 w-72 lg:h-32 xl:w-80"
key={schedule.id}
status="scheduled"
title={schedule.name}
timestamp={schedule.next_run_time}
selected={selectedView.id === schedule.id}
onClick={() => onSelectSchedule(schedule)}
onDelete={() => onDeleteSchedule(schedule.id)}
onPinAsPreset={() => onPinAsPreset(run)}
/>
))}
</>
) : (
schedules
.filter((schedule) => schedule.graph_id === agent.graph_id)
.map((schedule) => (
<AgentRunSummaryCard
className="h-28 w-72 lg:h-30 xl:w-80"
key={schedule.id}
type="schedule"
title={schedule.name}
timestamp={schedule.next_run_time}
selected={selectedView.id === schedule.id}
onClick={() => onSelectSchedule(schedule)}
onDelete={() => onDeleteSchedule(schedule.id)}
/>
))
)}
</div>
</ScrollArea>
</aside>

View File

@@ -1,10 +1,5 @@
import React, { useCallback, useEffect, useState } from "react";
import {
GraphExecutionMeta,
LibraryAgent,
NodeExecutionResult,
SpecialBlockID,
} from "@/lib/autogpt-server-api";
import { GraphExecutionMeta, LibraryAgent } from "@/lib/autogpt-server-api";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import Link from "next/link";
import { Button, buttonVariants } from "@/components/ui/button";

View File

@@ -7,7 +7,6 @@ import {
APIKeyPermission,
Block,
CreateAPIKeyResponse,
CreateLibraryAgentPresetRequest,
CreatorDetails,
CreatorsResponse,
Credentials,
@@ -25,15 +24,22 @@ import {
LibraryAgent,
LibraryAgentID,
LibraryAgentPreset,
LibraryAgentPresetCreatable,
LibraryAgentPresetCreatableFromGraphExecution,
LibraryAgentPresetID,
LibraryAgentPresetResponse,
LibraryAgentPresetUpdatable,
LibraryAgentResponse,
LibraryAgentSortEnum,
MyAgentsResponse,
NodeExecutionResult,
NotificationPreference,
NotificationPreferenceDTO,
OttoQuery,
OttoResponse,
ProfileDetails,
RefundRequest,
ReviewSubmissionRequest,
Schedule,
ScheduleCreatable,
ScheduleID,
@@ -45,14 +51,11 @@ import {
StoreSubmission,
StoreSubmissionRequest,
StoreSubmissionsResponse,
SubmissionStatus,
TransactionHistory,
User,
UserPasswordCredentials,
OttoQuery,
OttoResponse,
UserOnboarding,
ReviewSubmissionRequest,
SubmissionStatus,
UserPasswordCredentials,
} from "./types";
import { createBrowserClient } from "@supabase/ssr";
import getServerSupabase from "../supabase/getServerSupabase";
@@ -593,42 +596,63 @@ export default class BackendAPI {
await this._request("PUT", `/library/agents/${libraryAgentId}`, params);
}
listLibraryAgentPresets(params?: {
async listLibraryAgentPresets(params?: {
graph_id?: GraphID;
page?: number;
page_size?: number;
}): Promise<LibraryAgentPresetResponse> {
return this._get("/library/presets", params);
const response: LibraryAgentPresetResponse = await this._get(
"/library/presets",
params,
);
return {
...response,
presets: response.presets.map(parseLibraryAgentPresetTimestamp),
};
}
getLibraryAgentPreset(presetId: string): Promise<LibraryAgentPreset> {
return this._get(`/library/presets/${presetId}`);
}
createLibraryAgentPreset(
preset: CreateLibraryAgentPresetRequest,
async getLibraryAgentPreset(
presetID: LibraryAgentPresetID,
): Promise<LibraryAgentPreset> {
return this._request("POST", "/library/presets", preset);
const preset = await this._get(`/library/presets/${presetID}`);
return parseLibraryAgentPresetTimestamp(preset);
}
updateLibraryAgentPreset(
presetId: string,
preset: CreateLibraryAgentPresetRequest,
async createLibraryAgentPreset(
params:
| LibraryAgentPresetCreatable
| LibraryAgentPresetCreatableFromGraphExecution,
): Promise<LibraryAgentPreset> {
return this._request("PUT", `/library/presets/${presetId}`, preset);
const new_preset = await this._request("POST", "/library/presets", params);
return parseLibraryAgentPresetTimestamp(new_preset);
}
async deleteLibraryAgentPreset(presetId: string): Promise<void> {
await this._request("DELETE", `/library/presets/${presetId}`);
async updateLibraryAgentPreset(
presetID: LibraryAgentPresetID,
partial_preset: LibraryAgentPresetUpdatable,
): Promise<LibraryAgentPreset> {
const updated_preset = await this._request(
"PATCH",
`/library/presets/${presetID}`,
partial_preset,
);
return parseLibraryAgentPresetTimestamp(updated_preset);
}
async deleteLibraryAgentPreset(
presetID: LibraryAgentPresetID,
): Promise<void> {
await this._request("DELETE", `/library/presets/${presetID}`);
}
executeLibraryAgentPreset(
presetId: string,
graphId: GraphID,
presetID: LibraryAgentPresetID,
graphID: GraphID,
graphVersion: number,
nodeInput: { [key: string]: any },
): Promise<{ id: string }> {
return this._request("POST", `/library/presets/${presetId}/execute`, {
graph_id: graphId,
): Promise<{ id: GraphExecutionID }> {
return this._request("POST", `/library/presets/${presetID}/execute`, {
graph_id: graphID,
graph_version: graphVersion,
node_input: nodeInput,
});
@@ -1026,6 +1050,10 @@ function parseScheduleTimestamp(result: any): Schedule {
return _parseObjectTimestamps<Schedule>(result, ["next_run_time"]);
}
function parseLibraryAgentPresetTimestamp(result: any): LibraryAgentPreset {
return _parseObjectTimestamps<LibraryAgentPreset>(result, ["updated_at"]);
}
function _parseObjectTimestamps<T>(obj: any, keys: (keyof T)[]): T {
const result = { ...obj };
keys.forEach(

View File

@@ -252,7 +252,7 @@ export type GraphExecutionMeta = {
user_id: UserID;
graph_id: GraphID;
graph_version: number;
preset_id?: string;
preset_id?: LibraryAgentPresetID;
status: "QUEUED" | "RUNNING" | "COMPLETED" | "TERMINATED" | "FAILED";
started_at: Date;
ended_at: Date;
@@ -380,7 +380,8 @@ export enum AgentStatus {
ERROR = "ERROR",
}
export interface LibraryAgentResponse {
/* Mirror of backend/server/v2/library/model.py:LibraryAgentResponse */
export type LibraryAgentResponse = {
agents: LibraryAgent[];
pagination: {
current_page: number;
@@ -388,37 +389,54 @@ export interface LibraryAgentResponse {
total_items: number;
total_pages: number;
};
}
};
export interface LibraryAgentPreset {
id: string;
/* Mirror of backend/server/v2/library/model.py:LibraryAgentPreset */
export type LibraryAgentPreset = {
id: LibraryAgentPresetID;
updated_at: Date;
graph_id: GraphID;
graph_version: number;
inputs: { [key: string]: any };
name: string;
description: string;
is_active: boolean;
inputs: { [key: string]: any };
}
};
export interface LibraryAgentPresetResponse {
export type LibraryAgentPresetID = Brand<string, "LibraryAgentPresetID">;
/* Mirror of backend/server/v2/library/model.py:LibraryAgentPresetResponse */
export type LibraryAgentPresetResponse = {
presets: LibraryAgentPreset[];
pagination: {
total: number;
page: number;
size: number;
};
}
};
export interface CreateLibraryAgentPresetRequest {
name: string;
description: string;
inputs: { [key: string]: any };
graph_id: GraphID;
graph_version: number;
is_active: boolean;
}
/* Mirror of backend/server/v2/library/model.py:LibraryAgentPresetCreatable */
export type LibraryAgentPresetCreatable = Omit<
LibraryAgentPreset,
"id" | "updated_at" | "is_active"
> & {
is_active?: boolean;
};
/* Mirror of backend/server/v2/library/model.py:LibraryAgentPresetCreatableFromGraphExecution */
export type LibraryAgentPresetCreatableFromGraphExecution = Omit<
LibraryAgentPresetCreatable,
"graph_id" | "graph_version" | "inputs"
> & {
graph_execution_id: GraphExecutionID;
};
/* Mirror of backend/server/v2/library/model.py:LibraryAgentPresetUpdatable */
export type LibraryAgentPresetUpdatable = Partial<
Omit<LibraryAgentPresetCreatable, "graph_id" | "graph_version">
>;
/* Mirror of backend/server/v2/library/model.py:LibraryAgentSort */
export enum LibraryAgentSortEnum {
CREATED_AT = "createdAt",
UPDATED_AT = "updatedAt",

View File

@@ -93,6 +93,7 @@ const config = {
20: "5rem",
24: "6rem",
28: "7rem",
30: "7.5rem",
32: "8rem",
36: "9rem",
40: "10rem",