refactor(backend): Clean up Library & Store DB schema (#9774)

Distilled from #9541 to reduce the scope of that PR.

- Part of #9307

-  Blocks #9786
  -  Blocks #9541

### Changes 🏗️

- Fix `LibraryAgent` schema (for #9786)
- Fix relationships between `LibraryAgent`, `AgentGraph`, and
`AgentPreset`
  - Impose uniqueness constraint on `LibraryAgent`

- Rename things that are called `agent` that actually refer to a
`graph`/`agentGraph`
- Fix singular/plural forms in DB schema
- Simplify reference names of closely related objects (e.g.
`AgentGraph.AgentGraphExecutions` -> `AgentGraph.Executions`)

- Eliminate use of `# type: ignore` in DB statements
  - Add `typed` and `typed_cast` utilities to `backend.util.type`

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
- [x] CI static type checking (with all risky `# type: ignore` removed)
  - [x] Check that column references in views are updated
This commit is contained in:
Reinier van der Leer
2025-04-10 12:40:25 +02:00
committed by GitHub
parent 70890dee43
commit 353396110c
29 changed files with 441 additions and 295 deletions

View File

@@ -3,6 +3,7 @@ import logging
from abc import ABC, abstractmethod
from collections import defaultdict
from datetime import datetime, timezone
from typing import cast
import stripe
from autogpt_libs.utils.cache import thread_cached
@@ -18,6 +19,7 @@ from prisma.types import (
CreditRefundRequestCreateInput,
CreditTransactionCreateInput,
CreditTransactionWhereInput,
IntFilter,
)
from tenacity import retry, stop_after_attempt, wait_exponential
@@ -213,7 +215,7 @@ class UserCreditBase(ABC):
"userId": user_id,
"createdAt": {"lte": top_time},
"isActive": True,
"runningBalance": {"not": None}, # type: ignore
"runningBalance": cast(IntFilter, {"not": None}),
},
order={"createdAt": "desc"},
)

View File

@@ -23,6 +23,7 @@ from prisma.models import (
AgentNodeExecutionInputOutput,
)
from prisma.types import (
AgentGraphExecutionCreateInput,
AgentGraphExecutionWhereInput,
AgentNodeExecutionCreateInput,
AgentNodeExecutionInputOutputCreateInput,
@@ -121,7 +122,7 @@ class GraphExecution(GraphExecutionMeta):
@staticmethod
def from_db(_graph_exec: AgentGraphExecution):
if _graph_exec.AgentNodeExecutions is None:
if _graph_exec.NodeExecutions is None:
raise ValueError("Node executions must be included in query")
graph_exec = GraphExecutionMeta.from_db(_graph_exec)
@@ -129,7 +130,7 @@ class GraphExecution(GraphExecutionMeta):
complete_node_executions = sorted(
[
NodeExecutionResult.from_db(ne, _graph_exec.userId)
for ne in _graph_exec.AgentNodeExecutions
for ne in _graph_exec.NodeExecutions
if ne.executionStatus != ExecutionStatus.INCOMPLETE
],
key=lambda ne: (ne.queue_time is None, ne.queue_time or ne.add_time),
@@ -181,7 +182,7 @@ class GraphExecutionWithNodes(GraphExecution):
@staticmethod
def from_db(_graph_exec: AgentGraphExecution):
if _graph_exec.AgentNodeExecutions is None:
if _graph_exec.NodeExecutions is None:
raise ValueError("Node executions must be included in query")
graph_exec_with_io = GraphExecution.from_db(_graph_exec)
@@ -189,7 +190,7 @@ class GraphExecutionWithNodes(GraphExecution):
node_executions = sorted(
[
NodeExecutionResult.from_db(ne, _graph_exec.userId)
for ne in _graph_exec.AgentNodeExecutions
for ne in _graph_exec.NodeExecutions
],
key=lambda ne: (ne.queue_time is None, ne.queue_time or ne.add_time),
)
@@ -220,21 +221,21 @@ class NodeExecutionResult(BaseModel):
end_time: datetime | None
@staticmethod
def from_db(execution: AgentNodeExecution, user_id: Optional[str] = None):
if execution.executionData:
def from_db(_node_exec: AgentNodeExecution, user_id: Optional[str] = None):
if _node_exec.executionData:
# Execution that has been queued for execution will persist its data.
input_data = type_utils.convert(execution.executionData, dict[str, Any])
input_data = type_utils.convert(_node_exec.executionData, dict[str, Any])
else:
# For incomplete execution, executionData will not be yet available.
input_data: BlockInput = defaultdict()
for data in execution.Input or []:
for data in _node_exec.Input or []:
input_data[data.name] = type_utils.convert(data.data, type[Any])
output_data: CompletedBlockOutput = defaultdict(list)
for data in execution.Output or []:
for data in _node_exec.Output or []:
output_data[data.name].append(type_utils.convert(data.data, type[Any]))
graph_execution: AgentGraphExecution | None = execution.AgentGraphExecution
graph_execution: AgentGraphExecution | None = _node_exec.GraphExecution
if graph_execution:
user_id = graph_execution.userId
elif not user_id:
@@ -246,17 +247,17 @@ class NodeExecutionResult(BaseModel):
user_id=user_id,
graph_id=graph_execution.agentGraphId if graph_execution else "",
graph_version=graph_execution.agentGraphVersion if graph_execution else 0,
graph_exec_id=execution.agentGraphExecutionId,
block_id=execution.AgentNode.agentBlockId if execution.AgentNode else "",
node_exec_id=execution.id,
node_id=execution.agentNodeId,
status=execution.executionStatus,
graph_exec_id=_node_exec.agentGraphExecutionId,
block_id=_node_exec.Node.agentBlockId if _node_exec.Node else "",
node_exec_id=_node_exec.id,
node_id=_node_exec.agentNodeId,
status=_node_exec.executionStatus,
input_data=input_data,
output_data=output_data,
add_time=execution.addedTime,
queue_time=execution.queuedTime,
start_time=execution.startedTime,
end_time=execution.endedTime,
add_time=_node_exec.addedTime,
queue_time=_node_exec.queuedTime,
start_time=_node_exec.startedTime,
end_time=_node_exec.endedTime,
)
@@ -351,29 +352,29 @@ async def create_graph_execution(
The id of the AgentGraphExecution and the list of ExecutionResult for each node.
"""
result = await AgentGraphExecution.prisma().create(
data={
"agentGraphId": graph_id,
"agentGraphVersion": graph_version,
"executionStatus": ExecutionStatus.QUEUED,
"AgentNodeExecutions": {
"create": [ # type: ignore
{
"agentNodeId": node_id,
"executionStatus": ExecutionStatus.QUEUED,
"queuedTime": datetime.now(tz=timezone.utc),
"Input": {
data=AgentGraphExecutionCreateInput(
agentGraphId=graph_id,
agentGraphVersion=graph_version,
executionStatus=ExecutionStatus.QUEUED,
NodeExecutions={
"create": [
AgentNodeExecutionCreateInput(
agentNodeId=node_id,
executionStatus=ExecutionStatus.QUEUED,
queuedTime=datetime.now(tz=timezone.utc),
Input={
"create": [
{"name": name, "data": Json(data)}
for name, data in node_input.items()
]
},
}
)
for node_id, node_input in nodes_input
]
},
"userId": user_id,
"agentPresetId": preset_id,
},
userId=user_id,
agentPresetId=preset_id,
),
include=GRAPH_EXECUTION_INCLUDE_WITH_NODES,
)
@@ -600,7 +601,7 @@ async def get_node_execution_results(
"agentGraphExecutionId": graph_exec_id,
}
if block_ids:
where_clause["AgentNode"] = {"is": {"agentBlockId": {"in": block_ids}}}
where_clause["Node"] = {"is": {"agentBlockId": {"in": block_ids}}}
if statuses:
where_clause["OR"] = [{"executionStatus": status} for status in statuses]

View File

@@ -1,7 +1,7 @@
import logging
import uuid
from collections import defaultdict
from typing import Any, Literal, Optional, Type
from typing import Any, Literal, Optional, Type, cast
import prisma
from prisma import Json
@@ -10,7 +10,9 @@ from prisma.models import AgentGraph, AgentNode, AgentNodeLink, StoreListingVers
from prisma.types import (
AgentGraphCreateInput,
AgentGraphWhereInput,
AgentGraphWhereInputRecursive1,
AgentNodeCreateInput,
AgentNodeIncludeFromAgentNodeRecursive1,
AgentNodeLinkCreateInput,
)
from pydantic.fields import computed_field
@@ -465,13 +467,11 @@ class GraphModel(Graph):
is_active=graph.isActive,
name=graph.name or "",
description=graph.description or "",
nodes=[
NodeModel.from_db(node, for_export) for node in graph.AgentNodes or []
],
nodes=[NodeModel.from_db(node, for_export) for node in graph.Nodes or []],
links=list(
{
Link.from_db(link)
for node in graph.AgentNodes or []
for node in graph.Nodes or []
for link in (node.Input or []) + (node.Output or [])
}
),
@@ -602,8 +602,8 @@ async def get_graph(
and not (
await StoreListingVersion.prisma().find_first(
where={
"agentId": graph_id,
"agentVersion": version or graph.version,
"agentGraphId": graph_id,
"agentGraphVersion": version or graph.version,
"isDeleted": False,
"submissionStatus": SubmissionStatus.APPROVED,
}
@@ -637,12 +637,16 @@ async def get_sub_graphs(graph: AgentGraph) -> list[AgentGraph]:
sub_graph_ids = [
(graph_id, graph_version)
for graph in search_graphs
for node in graph.AgentNodes or []
for node in graph.Nodes or []
if (
node.AgentBlock
and node.AgentBlock.id == agent_block_id
and (graph_id := dict(node.constantInput).get("graph_id"))
and (graph_version := dict(node.constantInput).get("graph_version"))
and (graph_id := cast(str, dict(node.constantInput).get("graph_id")))
and (
graph_version := cast(
int, dict(node.constantInput).get("graph_version")
)
)
)
]
if not sub_graph_ids:
@@ -651,13 +655,16 @@ async def get_sub_graphs(graph: AgentGraph) -> list[AgentGraph]:
graphs = await AgentGraph.prisma().find_many(
where={
"OR": [
{
"id": graph_id,
"version": graph_version,
"userId": graph.userId, # Ensure the sub-graph is owned by the same user
}
type_utils.typed(
AgentGraphWhereInputRecursive1,
{
"id": graph_id,
"version": graph_version,
"userId": graph.userId, # Ensure the sub-graph is owned by the same user
},
)
for graph_id, graph_version in sub_graph_ids
] # type: ignore
]
},
include=AGENT_GRAPH_INCLUDE,
)
@@ -671,7 +678,13 @@ async def get_sub_graphs(graph: AgentGraph) -> list[AgentGraph]:
async def get_connected_output_nodes(node_id: str) -> list[tuple[Link, Node]]:
links = await AgentNodeLink.prisma().find_many(
where={"agentNodeSourceId": node_id},
include={"AgentNodeSink": {"include": AGENT_NODE_INCLUDE}}, # type: ignore
include={
"AgentNodeSink": {
"include": cast(
AgentNodeIncludeFromAgentNodeRecursive1, AGENT_NODE_INCLUDE
)
}
},
)
return [
(Link.from_db(link), NodeModel.from_db(link.AgentNodeSink))
@@ -829,12 +842,12 @@ async def fix_llm_provider_credentials():
SELECT graph."userId" user_id,
node.id node_id,
node."constantInput" node_preset_input
FROM platform."AgentNode" node
LEFT JOIN platform."AgentGraph" graph
ON node."agentGraphId" = graph.id
WHERE node."constantInput"::jsonb->'credentials'->>'provider' = 'llm'
ORDER BY graph."userId";
"""
FROM platform."AgentNode" node
LEFT JOIN platform."AgentGraph" graph
ON node."agentGraphId" = graph.id
WHERE node."constantInput"::jsonb->'credentials'->>'provider' = 'llm'
ORDER BY graph."userId";
"""
)
logger.info(f"Fixing LLM credential inputs on {len(broken_nodes)} nodes")
except Exception as e:

View File

@@ -1,7 +1,10 @@
from typing import cast
import prisma.enums
import prisma.types
from backend.blocks.io import IO_BLOCK_IDs
from backend.util.type import typed_cast
AGENT_NODE_INCLUDE: prisma.types.AgentNodeInclude = {
"Input": True,
@@ -11,25 +14,31 @@ AGENT_NODE_INCLUDE: prisma.types.AgentNodeInclude = {
}
AGENT_GRAPH_INCLUDE: prisma.types.AgentGraphInclude = {
"AgentNodes": {"include": AGENT_NODE_INCLUDE} # type: ignore
"Nodes": {
"include": typed_cast(
prisma.types.AgentNodeIncludeFromAgentNodeRecursive1,
prisma.types.AgentNodeIncludeFromAgentNode,
AGENT_NODE_INCLUDE,
)
}
}
EXECUTION_RESULT_INCLUDE: prisma.types.AgentNodeExecutionInclude = {
"Input": True,
"Output": True,
"AgentNode": True,
"AgentGraphExecution": True,
"Node": True,
"GraphExecution": True,
}
MAX_NODE_EXECUTIONS_FETCH = 1000
GRAPH_EXECUTION_INCLUDE_WITH_NODES: prisma.types.AgentGraphExecutionInclude = {
"AgentNodeExecutions": {
"NodeExecutions": {
"include": {
"Input": True,
"Output": True,
"AgentNode": True,
"AgentGraphExecution": True,
"Node": True,
"GraphExecution": True,
},
"order_by": [
{"queuedTime": "desc"},
@@ -41,31 +50,42 @@ GRAPH_EXECUTION_INCLUDE_WITH_NODES: prisma.types.AgentGraphExecutionInclude = {
}
GRAPH_EXECUTION_INCLUDE: prisma.types.AgentGraphExecutionInclude = {
"AgentNodeExecutions": {
**GRAPH_EXECUTION_INCLUDE_WITH_NODES["AgentNodeExecutions"], # type: ignore
"NodeExecutions": {
**cast(
prisma.types.FindManyAgentNodeExecutionArgsFromAgentGraphExecution,
GRAPH_EXECUTION_INCLUDE_WITH_NODES["NodeExecutions"],
),
"where": {
"AgentNode": {
"AgentBlock": {"id": {"in": IO_BLOCK_IDs}}, # type: ignore
},
"NOT": {
"executionStatus": prisma.enums.AgentExecutionStatus.INCOMPLETE,
},
"Node": typed_cast(
prisma.types.AgentNodeRelationFilter,
prisma.types.AgentNodeWhereInput,
{
"AgentBlock": {"id": {"in": IO_BLOCK_IDs}},
},
),
"NOT": [{"executionStatus": prisma.enums.AgentExecutionStatus.INCOMPLETE}],
},
}
}
INTEGRATION_WEBHOOK_INCLUDE: prisma.types.IntegrationWebhookInclude = {
"AgentNodes": {"include": AGENT_NODE_INCLUDE} # type: ignore
"AgentNodes": {
"include": typed_cast(
prisma.types.AgentNodeIncludeFromAgentNodeRecursive1,
prisma.types.AgentNodeInclude,
AGENT_NODE_INCLUDE,
)
}
}
def library_agent_include(user_id: str) -> prisma.types.LibraryAgentInclude:
return {
"Agent": {
"AgentGraph": {
"include": {
**AGENT_GRAPH_INCLUDE,
"AgentGraphExecution": {"where": {"userId": user_id}},
"Executions": {"where": {"userId": user_id}},
}
},
"Creator": True,

View File

@@ -6,7 +6,7 @@ import pydantic
from prisma import Json
from prisma.enums import OnboardingStep
from prisma.models import UserOnboarding
from prisma.types import UserOnboardingUpdateInput
from prisma.types import UserOnboardingCreateInput, UserOnboardingUpdateInput
from backend.data.block import get_blocks
from backend.data.graph import GraphModel
@@ -38,7 +38,7 @@ async def get_user_onboarding(user_id: str):
return await UserOnboarding.prisma().upsert(
where={"userId": user_id},
data={
"create": {"userId": user_id}, # type: ignore
"create": UserOnboardingCreateInput(userId=user_id),
"update": {},
},
)
@@ -186,11 +186,11 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
where={
"id": {"in": [agent.storeListingVersionId for agent in storeAgents]},
},
include={"Agent": True},
include={"AgentGraph": True},
)
for listing in agentListings:
agent = listing.Agent
agent = listing.AgentGraph
if agent is None:
continue
graph = GraphModel.from_db(agent)

View File

@@ -11,7 +11,7 @@ from fastapi import HTTPException
from prisma import Json
from prisma.enums import NotificationType
from prisma.models import User
from prisma.types import UserCreateInput, UserUpdateInput
from prisma.types import JsonFilter, UserCreateInput, UserUpdateInput
from backend.data.db import prisma
from backend.data.model import UserIntegrations, UserMetadata, UserMetadataRaw
@@ -135,10 +135,15 @@ async def migrate_and_encrypt_user_integrations():
"""Migrate integration credentials and OAuth states from metadata to integrations column."""
users = await User.prisma().find_many(
where={
"metadata": {
"path": ["integration_credentials"],
"not": Json({"a": "yolo"}), # bogus value works to check if key exists
} # type: ignore
"metadata": cast(
JsonFilter,
{
"path": ["integration_credentials"],
"not": Json(
{"a": "yolo"}
), # bogus value works to check if key exists
},
)
}
)
logger.info(f"Migrating integration credentials for {len(users)} users")

View File

@@ -6,7 +6,6 @@ import prisma.errors
import prisma.fields
import prisma.models
import prisma.types
from prisma.types import AgentPresetCreateInput
import backend.data.graph
import backend.server.model
@@ -69,12 +68,12 @@ async def list_library_agents(
if search_term:
where_clause["OR"] = [
{
"Agent": {
"AgentGraph": {
"is": {"name": {"contains": search_term, "mode": "insensitive"}}
}
},
{
"Agent": {
"AgentGraph": {
"is": {
"description": {"contains": search_term, "mode": "insensitive"}
}
@@ -233,7 +232,8 @@ async def create_library_agent(
isCreatedByUser=(user_id == graph.user_id),
useGraphIsActiveVersion=True,
User={"connect": {"id": user_id}},
Agent={
# Creator={"connect": {"id": agent.userId}},
AgentGraph={
"connect": {
"graphVersionId": {"id": graph.id, "version": graph.version}
}
@@ -247,38 +247,41 @@ async def create_library_agent(
async def update_agent_version_in_library(
user_id: str,
agent_id: str,
agent_version: int,
agent_graph_id: str,
agent_graph_version: int,
) -> None:
"""
Updates the agent version in the library if useGraphIsActiveVersion is True.
Args:
user_id: Owner of the LibraryAgent.
agent_id: The agent's ID to update.
agent_version: The new version of the agent.
agent_graph_id: The agent graph's ID to update.
agent_graph_version: The new version of the agent graph.
Raises:
DatabaseError: If there's an error with the update.
"""
logger.debug(
f"Updating agent version in library for user #{user_id}, "
f"agent #{agent_id} v{agent_version}"
f"agent #{agent_graph_id} v{agent_graph_version}"
)
try:
library_agent = await prisma.models.LibraryAgent.prisma().find_first_or_raise(
where={
"userId": user_id,
"agentId": agent_id,
"agentGraphId": agent_graph_id,
"useGraphIsActiveVersion": True,
},
)
await prisma.models.LibraryAgent.prisma().update(
where={"id": library_agent.id},
data={
"Agent": {
"AgentGraph": {
"connect": {
"graphVersionId": {"id": agent_id, "version": agent_version}
"graphVersionId": {
"id": agent_graph_id,
"version": agent_graph_version,
}
},
},
},
@@ -342,7 +345,7 @@ async def delete_library_agent_by_graph_id(graph_id: str, user_id: str) -> None:
"""
try:
await prisma.models.LibraryAgent.prisma().delete_many(
where={"agentId": graph_id, "userId": user_id}
where={"agentGraphId": graph_id, "userId": user_id}
)
except prisma.errors.PrismaError as e:
logger.error(f"Database error deleting library agent: {e}")
@@ -375,10 +378,10 @@ async def add_store_agent_to_library(
async with locked_transaction(f"add_agent_trx_{user_id}"):
store_listing_version = (
await prisma.models.StoreListingVersion.prisma().find_unique(
where={"id": store_listing_version_id}, include={"Agent": True}
where={"id": store_listing_version_id}, include={"AgentGraph": True}
)
)
if not store_listing_version or not store_listing_version.Agent:
if not store_listing_version or not store_listing_version.AgentGraph:
logger.warning(
f"Store listing version not found: {store_listing_version_id}"
)
@@ -386,7 +389,7 @@ async def add_store_agent_to_library(
f"Store listing version {store_listing_version_id} not found or invalid"
)
graph = store_listing_version.Agent
graph = store_listing_version.AgentGraph
if graph.userId == user_id:
logger.warning(
f"User #{user_id} attempted to add their own agent to their library"
@@ -398,8 +401,8 @@ async def add_store_agent_to_library(
await prisma.models.LibraryAgent.prisma().find_first(
where={
"userId": user_id,
"agentId": graph.id,
"agentVersion": graph.version,
"agentGraphId": graph.id,
"agentGraphVersion": graph.version,
},
include=library_agent_include(user_id),
)
@@ -421,15 +424,15 @@ async def add_store_agent_to_library(
added_agent = await prisma.models.LibraryAgent.prisma().create(
data=prisma.types.LibraryAgentCreateInput(
userId=user_id,
agentId=graph.id,
agentVersion=graph.version,
agentGraphId=graph.id,
agentGraphVersion=graph.version,
isCreatedByUser=False,
),
include=library_agent_include(user_id),
)
logger.debug(
f"Added graph #{graph.id} "
f"for store listing #{store_listing_version.id} "
f"Added graph #{graph.id} v{graph.version}"
f"for store listing version #{store_listing_version.id} "
f"to library for user #{user_id}"
)
return library_model.LibraryAgent.from_db(added_agent)
@@ -468,8 +471,8 @@ async def set_is_deleted_for_library_agent(
count = await prisma.models.LibraryAgent.prisma().update_many(
where={
"userId": user_id,
"agentId": agent_id,
"agentVersion": agent_version,
"agentGraphId": agent_id,
"agentGraphVersion": agent_version,
},
data={"isDeleted": is_deleted},
)
@@ -598,21 +601,22 @@ async def upsert_preset(
f"Upserting 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=AgentPresetCreateInput(
name=preset.name,
description=preset.description,
isActive=preset.is_active,
InputPresets={
"create": [
{"name": name, "data": prisma.fields.Json(data)}
for name, data in preset.inputs.items()
]
},
),
data={
"name": preset.name,
"description": preset.description,
"isActive": preset.is_active,
"InputPresets": {"create": inputs},
},
include={"InputPresets": True},
)
if not updated:
@@ -625,15 +629,10 @@ async def upsert_preset(
userId=user_id,
name=preset.name,
description=preset.description,
agentId=preset.agent_id,
agentVersion=preset.agent_version,
agentGraphId=preset.graph_id,
agentGraphVersion=preset.graph_version,
isActive=preset.is_active,
InputPresets={
"create": [
{"name": name, "data": prisma.fields.Json(data)}
for name, data in preset.inputs.items()
]
},
InputPresets={"create": inputs},
),
include={"InputPresets": True},
)

View File

@@ -30,8 +30,8 @@ async def test_get_library_agents(mocker):
prisma.models.LibraryAgent(
id="ua1",
userId="test-user",
agentId="agent2",
agentVersion=1,
agentGraphId="agent2",
agentGraphVersion=1,
isCreatedByUser=False,
isDeleted=False,
isArchived=False,
@@ -39,7 +39,7 @@ async def test_get_library_agents(mocker):
updatedAt=datetime.now(),
isFavorite=False,
useGraphIsActiveVersion=True,
Agent=prisma.models.AgentGraph(
AgentGraph=prisma.models.AgentGraph(
id="agent2",
version=1,
name="Test Agent 2",
@@ -71,8 +71,8 @@ async def test_get_library_agents(mocker):
assert result.agents[0].id == "ua1"
assert result.agents[0].name == "Test Agent 2"
assert result.agents[0].description == "Test Description 2"
assert result.agents[0].agent_id == "agent2"
assert result.agents[0].agent_version == 1
assert result.agents[0].graph_id == "agent2"
assert result.agents[0].graph_version == 1
assert result.agents[0].can_access_graph is False
assert result.agents[0].is_latest_version is True
assert result.pagination.total_items == 1
@@ -90,8 +90,8 @@ async def test_add_agent_to_library(mocker):
version=1,
createdAt=datetime.now(),
updatedAt=datetime.now(),
agentId="agent1",
agentVersion=1,
agentGraphId="agent1",
agentGraphVersion=1,
name="Test Agent",
subHeading="Test Agent Subheading",
imageUrls=["https://example.com/image.jpg"],
@@ -102,7 +102,7 @@ async def test_add_agent_to_library(mocker):
isAvailable=True,
storeListingId="listing123",
submissionStatus=prisma.enums.SubmissionStatus.APPROVED,
Agent=prisma.models.AgentGraph(
AgentGraph=prisma.models.AgentGraph(
id="agent1",
version=1,
name="Test Agent",
@@ -116,8 +116,8 @@ async def test_add_agent_to_library(mocker):
mock_library_agent_data = prisma.models.LibraryAgent(
id="ua1",
userId="test-user",
agentId=mock_store_listing_data.agentId,
agentVersion=1,
agentGraphId=mock_store_listing_data.agentGraphId,
agentGraphVersion=1,
isCreatedByUser=False,
isDeleted=False,
isArchived=False,
@@ -125,7 +125,7 @@ async def test_add_agent_to_library(mocker):
updatedAt=datetime.now(),
isFavorite=False,
useGraphIsActiveVersion=True,
Agent=mock_store_listing_data.Agent,
AgentGraph=mock_store_listing_data.AgentGraph,
)
# Mock prisma calls
@@ -147,19 +147,22 @@ async def test_add_agent_to_library(mocker):
# Verify mocks called correctly
mock_store_listing_version.return_value.find_unique.assert_called_once_with(
where={"id": "version123"}, include={"Agent": True}
where={"id": "version123"}, include={"AgentGraph": True}
)
mock_library_agent.return_value.find_first.assert_called_once_with(
where={
"userId": "test-user",
"agentId": "agent1",
"agentVersion": 1,
"agentGraphId": "agent1",
"agentGraphVersion": 1,
},
include=library_agent_include("test-user"),
)
mock_library_agent.return_value.create.assert_called_once_with(
data=prisma.types.LibraryAgentCreateInput(
userId="test-user", agentId="agent1", agentVersion=1, isCreatedByUser=False
userId="test-user",
agentGraphId="agent1",
agentGraphVersion=1,
isCreatedByUser=False,
),
include=library_agent_include("test-user"),
)
@@ -182,5 +185,5 @@ async def test_add_agent_to_library_not_found(mocker):
# Verify mock called correctly
mock_store_listing_version.return_value.find_unique.assert_called_once_with(
where={"id": "version123"}, include={"Agent": True}
where={"id": "version123"}, include={"AgentGraph": True}
)

View File

@@ -25,8 +25,8 @@ class LibraryAgent(pydantic.BaseModel):
"""
id: str
agent_id: str
agent_version: int
graph_id: str
graph_version: int
image_url: str | None
@@ -58,12 +58,12 @@ class LibraryAgent(pydantic.BaseModel):
Factory method that constructs a LibraryAgent from a Prisma LibraryAgent
model instance.
"""
if not agent.Agent:
if not agent.AgentGraph:
raise ValueError("Associated Agent record is required.")
graph = graph_model.GraphModel.from_db(agent.Agent)
graph = graph_model.GraphModel.from_db(agent.AgentGraph)
agent_updated_at = agent.Agent.updatedAt
agent_updated_at = agent.AgentGraph.updatedAt
lib_agent_updated_at = agent.updatedAt
# Compute updated_at as the latest between library agent and graph
@@ -83,21 +83,21 @@ class LibraryAgent(pydantic.BaseModel):
week_ago = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(
days=7
)
executions = agent.Agent.AgentGraphExecution or []
executions = agent.AgentGraph.Executions or []
status_result = _calculate_agent_status(executions, week_ago)
status = status_result.status
new_output = status_result.new_output
# Check if user can access the graph
can_access_graph = agent.Agent.userId == agent.userId
can_access_graph = agent.AgentGraph.userId == agent.userId
# Hard-coded to True until a method to check is implemented
is_latest_version = True
return LibraryAgent(
id=agent.id,
agent_id=agent.agentId,
agent_version=agent.agentVersion,
graph_id=agent.agentGraphId,
graph_version=agent.agentGraphVersion,
image_url=agent.imageUrl,
creator_name=creator_name,
creator_image_url=creator_image_url,
@@ -174,8 +174,8 @@ class LibraryAgentPreset(pydantic.BaseModel):
id: str
updated_at: datetime.datetime
agent_id: str
agent_version: int
graph_id: str
graph_version: int
name: str
description: str
@@ -194,8 +194,8 @@ class LibraryAgentPreset(pydantic.BaseModel):
return cls(
id=preset.id,
updated_at=preset.updatedAt,
agent_id=preset.agentId,
agent_version=preset.agentVersion,
graph_id=preset.agentGraphId,
graph_version=preset.agentGraphVersion,
name=preset.name,
description=preset.description,
is_active=preset.isActive,
@@ -218,8 +218,8 @@ class CreateLibraryAgentPresetRequest(pydantic.BaseModel):
name: str
description: str
inputs: block_model.BlockInput
agent_id: str
agent_version: int
graph_id: str
graph_version: int
is_active: bool

View File

@@ -5,7 +5,6 @@ import prisma.models
import pytest
import backend.server.v2.library.model as library_model
from backend.util import json
@pytest.mark.asyncio
@@ -15,8 +14,8 @@ async def test_agent_preset_from_db():
id="test-agent-123",
createdAt=datetime.datetime.now(),
updatedAt=datetime.datetime.now(),
agentId="agent-123",
agentVersion=1,
agentGraphId="agent-123",
agentGraphVersion=1,
name="Test Agent",
description="Test agent description",
isActive=True,
@@ -27,7 +26,7 @@ async def test_agent_preset_from_db():
id="input-123",
time=datetime.datetime.now(),
name="input1",
data=json.dumps({"type": "string", "value": "test value"}), # type: ignore
data=prisma.Json({"type": "string", "value": "test value"}),
)
],
)
@@ -36,7 +35,7 @@ async def test_agent_preset_from_db():
agent = library_model.LibraryAgentPreset.from_db(db_agent)
assert agent.id == "test-agent-123"
assert agent.agent_version == 1
assert agent.graph_version == 1
assert agent.is_active is True
assert agent.name == "Test Agent"
assert agent.description == "Test agent description"

View File

@@ -35,8 +35,8 @@ async def test_get_library_agents_success(mocker: pytest_mock.MockFixture):
agents=[
library_model.LibraryAgent(
id="test-agent-1",
agent_id="test-agent-1",
agent_version=1,
graph_id="test-agent-1",
graph_version=1,
name="Test Agent 1",
description="Test Description 1",
image_url=None,
@@ -51,8 +51,8 @@ async def test_get_library_agents_success(mocker: pytest_mock.MockFixture):
),
library_model.LibraryAgent(
id="test-agent-2",
agent_id="test-agent-2",
agent_version=1,
graph_id="test-agent-2",
graph_version=1,
name="Test Agent 2",
description="Test Description 2",
image_url=None,
@@ -78,9 +78,9 @@ async def test_get_library_agents_success(mocker: pytest_mock.MockFixture):
data = library_model.LibraryAgentResponse.model_validate(response.json())
assert len(data.agents) == 2
assert data.agents[0].agent_id == "test-agent-1"
assert data.agents[0].graph_id == "test-agent-1"
assert data.agents[0].can_access_graph is True
assert data.agents[1].agent_id == "test-agent-2"
assert data.agents[1].graph_id == "test-agent-2"
assert data.agents[1].can_access_graph is False
mock_db_call.assert_called_once_with(
user_id="test-user-id",

View File

@@ -12,6 +12,7 @@ import backend.server.v2.store.exceptions
import backend.server.v2.store.model
from backend.data.graph import GraphModel, get_sub_graphs
from backend.data.includes import AGENT_GRAPH_INCLUDE
from backend.util.type import typed_cast
logger = logging.getLogger(__name__)
@@ -200,17 +201,17 @@ async def get_available_graph(
"isAvailable": True,
"isDeleted": False,
},
include={"Agent": {"include": {"AgentNodes": True}}},
include={"AgentGraph": {"include": {"Nodes": True}}},
)
)
if not store_listing_version or not store_listing_version.Agent:
if not store_listing_version or not store_listing_version.AgentGraph:
raise fastapi.HTTPException(
status_code=404,
detail=f"Store listing version {store_listing_version_id} not found",
)
graph = GraphModel.from_db(store_listing_version.Agent)
graph = GraphModel.from_db(store_listing_version.AgentGraph)
# We return graph meta, without nodes, they cannot be just removed
# because then input_schema would be empty
return {
@@ -516,7 +517,7 @@ async def delete_store_submission(
try:
# Verify the submission belongs to this user
submission = await prisma.models.StoreListing.prisma().find_first(
where={"agentId": submission_id, "owningUserId": user_id}
where={"agentGraphId": submission_id, "owningUserId": user_id}
)
if not submission:
@@ -598,7 +599,7 @@ async def create_store_submission(
# Check if listing already exists for this agent
existing_listing = await prisma.models.StoreListing.prisma().find_first(
where=prisma.types.StoreListingWhereInput(
agentId=agent_id, owningUserId=user_id
agentGraphId=agent_id, owningUserId=user_id
)
)
@@ -625,15 +626,15 @@ async def create_store_submission(
# If no existing listing, create a new one
data = prisma.types.StoreListingCreateInput(
slug=slug,
agentId=agent_id,
agentVersion=agent_version,
agentGraphId=agent_id,
agentGraphVersion=agent_version,
owningUserId=user_id,
createdAt=datetime.now(tz=timezone.utc),
Versions={
"create": [
prisma.types.StoreListingVersionCreateInput(
agentId=agent_id,
agentVersion=agent_version,
agentGraphId=agent_id,
agentGraphVersion=agent_version,
name=name,
videoUrl=video_url,
imageUrls=image_urls,
@@ -758,8 +759,8 @@ async def create_store_version(
new_version = await prisma.models.StoreListingVersion.prisma().create(
data=prisma.types.StoreListingVersionCreateInput(
version=next_version,
agentId=agent_id,
agentVersion=agent_version,
agentGraphId=agent_id,
agentGraphVersion=agent_version,
name=name,
videoUrl=video_url,
imageUrls=image_urls,
@@ -959,17 +960,17 @@ async def get_my_agents(
try:
search_filter: prisma.types.LibraryAgentWhereInput = {
"userId": user_id,
"Agent": {"is": {"StoreListing": {"none": {"isDeleted": False}}}},
"AgentGraph": {"is": {"StoreListing": {"none": {"isDeleted": False}}}},
"isArchived": False,
"isDeleted": False,
}
library_agents = await prisma.models.LibraryAgent.prisma().find_many(
where=search_filter,
order=[{"agentVersion": "desc"}],
order=[{"agentGraphVersion": "desc"}],
skip=(page - 1) * page_size,
take=page_size,
include={"Agent": True},
include={"AgentGraph": True},
)
total = await prisma.models.LibraryAgent.prisma().count(where=search_filter)
@@ -985,7 +986,7 @@ async def get_my_agents(
agent_image=library_agent.imageUrl,
)
for library_agent in library_agents
if (graph := library_agent.Agent)
if (graph := library_agent.AgentGraph)
]
return backend.server.v2.store.model.MyAgentsResponse(
@@ -1020,13 +1021,13 @@ async def get_agent(
graph = await backend.data.graph.get_graph(
user_id=user_id,
graph_id=store_listing_version.agentId,
version=store_listing_version.agentVersion,
graph_id=store_listing_version.agentGraphId,
version=store_listing_version.agentGraphVersion,
for_export=True,
)
if not graph:
raise ValueError(
f"Agent {store_listing_version.agentId} v{store_listing_version.agentVersion} not found"
f"Agent {store_listing_version.agentGraphId} v{store_listing_version.agentGraphVersion} not found"
)
return graph
@@ -1050,11 +1051,14 @@ async def _get_missing_sub_store_listing(
# Fetch all the sub-graphs that are listed, and return the ones missing.
store_listed_sub_graphs = {
(listing.agentId, listing.agentVersion)
(listing.agentGraphId, listing.agentGraphVersion)
for listing in await prisma.models.StoreListingVersion.prisma().find_many(
where={
"OR": [
{"agentId": sub_graph.id, "agentVersion": sub_graph.version}
{
"agentGraphId": sub_graph.id,
"agentGraphVersion": sub_graph.version,
}
for sub_graph in sub_graphs
],
"submissionStatus": prisma.enums.SubmissionStatus.APPROVED,
@@ -1084,7 +1088,13 @@ async def review_store_submission(
where={"id": store_listing_version_id},
include={
"StoreListing": True,
"Agent": {"include": AGENT_GRAPH_INCLUDE}, # type: ignore
"AgentGraph": {
"include": typed_cast(
prisma.types.AgentGraphIncludeFromAgentGraphRecursive1,
prisma.types.AgentGraphInclude,
AGENT_GRAPH_INCLUDE,
)
},
},
)
)
@@ -1096,23 +1106,23 @@ async def review_store_submission(
)
# If approving, update the listing to indicate it has an approved version
if is_approved and store_listing_version.Agent:
heading = f"Sub-graph of {store_listing_version.name}v{store_listing_version.agentVersion}"
if is_approved and store_listing_version.AgentGraph:
heading = f"Sub-graph of {store_listing_version.name}v{store_listing_version.agentGraphVersion}"
sub_store_listing_versions = [
prisma.types.StoreListingVersionCreateWithoutRelationsInput(
agentId=sub_graph.id,
agentVersion=sub_graph.version,
agentGraphId=sub_graph.id,
agentGraphVersion=sub_graph.version,
name=sub_graph.name or heading,
submissionStatus=prisma.enums.SubmissionStatus.APPROVED,
subHeading=heading,
description=f"{heading}: {sub_graph.description}",
changesSummary=f"This listing is added as a {heading} / #{store_listing_version.agentId}.",
changesSummary=f"This listing is added as a {heading} / #{store_listing_version.agentGraphId}.",
isAvailable=False, # Hide sub-graphs from the store by default.
submittedAt=datetime.now(tz=timezone.utc),
)
for sub_graph in await _get_missing_sub_store_listing(
store_listing_version.Agent
store_listing_version.AgentGraph
)
]
@@ -1155,8 +1165,8 @@ async def review_store_submission(
# Convert to Pydantic model for consistency
return backend.server.v2.store.model.StoreSubmission(
agent_id=submission.agentId,
agent_version=submission.agentVersion,
agent_id=submission.agentGraphId,
agent_version=submission.agentGraphVersion,
name=submission.name,
sub_heading=submission.subHeading,
slug=(
@@ -1294,8 +1304,8 @@ async def get_admin_listings_with_versions(
# If we have versions, turn them into StoreSubmission models
for version in listing.Versions or []:
version_model = backend.server.v2.store.model.StoreSubmission(
agent_id=version.agentId,
agent_version=version.agentVersion,
agent_id=version.agentGraphId,
agent_version=version.agentGraphVersion,
name=version.name,
sub_heading=version.subHeading,
slug=listing.slug,
@@ -1324,8 +1334,8 @@ async def get_admin_listings_with_versions(
backend.server.v2.store.model.StoreListingWithVersions(
listing_id=listing.id,
slug=listing.slug,
agent_id=listing.agentId,
agent_version=listing.agentVersion,
agent_id=listing.agentGraphId,
agent_version=listing.agentGraphVersion,
active_version_id=listing.activeVersionId,
has_approved_version=listing.hasApprovedVersion,
creator_email=creator_email,

View File

@@ -170,14 +170,14 @@ async def test_create_store_submission(mocker):
isDeleted=False,
hasApprovedVersion=False,
slug="test-agent",
agentId="agent-id",
agentVersion=1,
agentGraphId="agent-id",
agentGraphVersion=1,
owningUserId="user-id",
Versions=[
prisma.models.StoreListingVersion(
id="version-id",
agentId="agent-id",
agentVersion=1,
agentGraphId="agent-id",
agentGraphVersion=1,
name="Test Agent",
description="Test description",
createdAt=datetime.now(),

View File

@@ -182,6 +182,7 @@ def _try_convert(value: Any, target_type: Type, raise_on_mismatch: bool) -> Any:
T = TypeVar("T")
TT = TypeVar("TT")
def type_match(value: Any, target_type: Type[T]) -> T:
@@ -197,6 +198,18 @@ def convert(value: Any, target_type: Type[T]) -> T:
raise ConversionError(f"Failed to convert {value} to {target_type}") from e
def typed(type: type[T], value: T) -> T:
"""
Add an explicit type to a value. Useful in nested statements, e.g. dict literals.
"""
return value
def typed_cast(to_type: type[TT], from_type: type[T], value: T) -> TT:
"""Strict cast to preserve type checking abilities."""
return cast(TT, value)
class FormattedStringType(str):
string_format: str

View File

@@ -0,0 +1,50 @@
/*
Warnings:
- The relation LibraryAgent:AgentPreset was REMOVED
- A unique constraint covering the columns `[userId,agentGraphId,agentGraphVersion]` on the table `LibraryAgent` will be added. If there are existing duplicate values, this will fail.
- The foreign key constraints on AgentPreset and LibraryAgent are being changed from CASCADE to RESTRICT for AgentGraph deletion, which means you cannot delete AgentGraphs that have associated LibraryAgents or AgentPresets.
Use the following query to check whether these conditions are satisfied:
-- Check for duplicate LibraryAgent userId + agentGraphId + agentGraphVersion combinations that would violate the new unique constraint
SELECT la."userId",
la."agentId" as graph_id,
la."agentVersion" as graph_version,
COUNT(*) as multiplicity
FROM "LibraryAgent" la
GROUP BY la."userId",
la."agentId",
la."agentVersion"
HAVING COUNT(*) > 1;
*/
-- Drop foreign key constraints on columns we're about to rename
ALTER TABLE "AgentPreset" DROP CONSTRAINT "AgentPreset_agentId_agentVersion_fkey";
ALTER TABLE "LibraryAgent" DROP CONSTRAINT "LibraryAgent_agentId_agentVersion_fkey";
ALTER TABLE "LibraryAgent" DROP CONSTRAINT "LibraryAgent_agentPresetId_fkey";
-- Rename columns in AgentPreset
ALTER TABLE "AgentPreset" RENAME COLUMN "agentId" TO "agentGraphId";
ALTER TABLE "AgentPreset" RENAME COLUMN "agentVersion" TO "agentGraphVersion";
-- Rename columns in LibraryAgent
ALTER TABLE "LibraryAgent" RENAME COLUMN "agentId" TO "agentGraphId";
ALTER TABLE "LibraryAgent" RENAME COLUMN "agentVersion" TO "agentGraphVersion";
-- Drop LibraryAgent.agentPresetId column
ALTER TABLE "LibraryAgent" DROP COLUMN "agentPresetId";
-- Replace userId index with unique index on userId + agentGraphId + agentGraphVersion
DROP INDEX "LibraryAgent_userId_idx";
CREATE UNIQUE INDEX "LibraryAgent_userId_agentGraphId_agentGraphVersion_key" ON "LibraryAgent"("userId", "agentGraphId", "agentGraphVersion");
-- Re-add the foreign key constraints with new column names
ALTER TABLE "LibraryAgent" ADD CONSTRAINT "LibraryAgent_agentGraphId_agentGraphVersion_fkey"
FOREIGN KEY ("agentGraphId", "agentGraphVersion") REFERENCES "AgentGraph"("id", "version")
ON DELETE RESTRICT -- Disallow deleting AgentGraph when still referenced by existing LibraryAgents
ON UPDATE CASCADE;
ALTER TABLE "AgentPreset" ADD CONSTRAINT "AgentPreset_agentGraphId_agentGraphVersion_fkey"
FOREIGN KEY ("agentGraphId", "agentGraphVersion") REFERENCES "AgentGraph"("id", "version")
ON DELETE RESTRICT -- Disallow deleting AgentGraph when still referenced by existing AgentPresets
ON UPDATE CASCADE;

View File

@@ -0,0 +1,35 @@
/*
- Rename column StoreListing.agentId to agentGraphId
- Rename column StoreListing.agentVersion to agentGraphVersion
- Rename column StoreListingVersion.agentId to agentGraphId
- Rename column StoreListingVersion.agentVersion to agentGraphVersion
*/
-- Drop foreign key constraints on columns we're about to rename
ALTER TABLE "StoreListing" DROP CONSTRAINT "StoreListing_agentId_agentVersion_fkey";
ALTER TABLE "StoreListingVersion" DROP CONSTRAINT "StoreListingVersion_agentId_agentVersion_fkey";
-- Drop indices on columns we're about to rename
DROP INDEX "StoreListing_agentId_key";
DROP INDEX "StoreListingVersion_agentId_agentVersion_idx";
-- Rename columns
ALTER TABLE "StoreListing" RENAME COLUMN "agentId" TO "agentGraphId";
ALTER TABLE "StoreListing" RENAME COLUMN "agentVersion" TO "agentGraphVersion";
ALTER TABLE "StoreListingVersion" RENAME COLUMN "agentId" TO "agentGraphId";
ALTER TABLE "StoreListingVersion" RENAME COLUMN "agentVersion" TO "agentGraphVersion";
-- Re-create indices with updated name on renamed columns
CREATE UNIQUE INDEX "StoreListing_agentGraphId_key" ON "StoreListing"("agentGraphId");
CREATE INDEX "StoreListingVersion_agentGraphId_agentGraphVersion_idx" ON "StoreListingVersion"("agentGraphId", "agentGraphVersion");
-- Re-create foreign key constraints with updated name on renamed columns
ALTER TABLE "StoreListing" ADD CONSTRAINT "StoreListing_agentGraphId_agentGraphVersion_fkey"
FOREIGN KEY ("agentGraphId", "agentGraphVersion") REFERENCES "AgentGraph"("id", "version")
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE "StoreListingVersion" ADD CONSTRAINT "StoreListingVersion_agentGraphId_agentGraphVersion_fkey"
FOREIGN KEY ("agentGraphId", "agentGraphVersion") REFERENCES "AgentGraph"("id", "version")
ON DELETE RESTRICT
ON UPDATE CASCADE;

View File

@@ -39,19 +39,19 @@ model User {
AgentGraphExecutions AgentGraphExecution[]
AnalyticsDetails AnalyticsDetails[]
AnalyticsMetrics AnalyticsMetrics[]
CreditTransaction CreditTransaction[]
CreditTransactions CreditTransaction[]
AgentPreset AgentPreset[]
LibraryAgent LibraryAgent[]
AgentPresets AgentPreset[]
LibraryAgents LibraryAgent[]
Profile Profile[]
UserOnboarding UserOnboarding?
StoreListing StoreListing[]
StoreListingReview StoreListingReview[]
StoreListings StoreListing[]
StoreListingReviews StoreListingReview[]
StoreVersionsReviewed StoreListingVersion[]
APIKeys APIKey[]
IntegrationWebhooks IntegrationWebhook[]
UserNotificationBatch UserNotificationBatch[]
NotificationBatches UserNotificationBatch[]
@@index([id])
@@index([email])
@@ -102,13 +102,13 @@ model AgentGraph {
// This allows us to delete user data with deleting the agent which maybe in use by other users
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
AgentNodes AgentNode[]
AgentGraphExecution AgentGraphExecution[]
Nodes AgentNode[]
Executions AgentGraphExecution[]
AgentPreset AgentPreset[]
LibraryAgent LibraryAgent[]
StoreListing StoreListing[]
StoreListingVersion StoreListingVersion[]
Presets AgentPreset[]
LibraryAgents LibraryAgent[]
StoreListings StoreListing[]
StoreListingVersions StoreListingVersion[]
@@id(name: "graphVersionId", [id, version])
@@index([userId, isActive])
@@ -139,13 +139,12 @@ model AgentPreset {
userId String
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
agentId String
agentVersion Int
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
agentGraphId String
agentGraphVersion Int
AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version], onDelete: Restrict)
InputPresets AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData")
LibraryAgents LibraryAgent[]
AgentExecution AgentGraphExecution[]
InputPresets AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData")
Executions AgentGraphExecution[]
isDeleted Boolean @default(false)
@@ -194,7 +193,7 @@ model UserNotificationBatch {
}
// For the library page
// It is a user controlled list of agents, that they will see in there library
// It is a user controlled list of agents, that they will see in their library
model LibraryAgent {
id String @id @default(uuid())
createdAt DateTime @default(now())
@@ -205,12 +204,9 @@ model LibraryAgent {
imageUrl String?
agentId String
agentVersion Int
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version])
agentPresetId String?
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
agentGraphId String
agentGraphVersion Int
AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version], onDelete: Restrict)
creatorId String?
Creator Profile? @relation(fields: [creatorId], references: [id])
@@ -222,7 +218,7 @@ model LibraryAgent {
isArchived Boolean @default(false)
isDeleted Boolean @default(false)
@@index([userId])
@@unique([userId, agentGraphId, agentGraphVersion])
}
////////////////////////////////////////////////////////////
@@ -256,7 +252,7 @@ model AgentNode {
metadata Json @default("{}")
ExecutionHistory AgentNodeExecution[]
Executions AgentNodeExecution[]
@@index([agentGraphId, agentGraphVersion])
@@index([agentBlockId])
@@ -323,15 +319,15 @@ model AgentGraphExecution {
agentGraphVersion Int @default(1)
AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version], onDelete: Cascade)
AgentNodeExecutions AgentNodeExecution[]
NodeExecutions AgentNodeExecution[]
// Link to User model -- Executed by this user
userId String
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
stats Json?
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
agentPresetId String?
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
@@index([agentGraphId, agentGraphVersion])
@@index([userId])
@@ -342,10 +338,10 @@ model AgentNodeExecution {
id String @id @default(uuid())
agentGraphExecutionId String
AgentGraphExecution AgentGraphExecution @relation(fields: [agentGraphExecutionId], references: [id], onDelete: Cascade)
GraphExecution AgentGraphExecution @relation(fields: [agentGraphExecutionId], references: [id], onDelete: Cascade)
agentNodeId String
AgentNode AgentNode @relation(fields: [agentNodeId], references: [id], onDelete: Cascade)
Node AgentNode @relation(fields: [agentNodeId], references: [id], onDelete: Cascade)
Input AgentNodeExecutionInputOutput[] @relation("AgentNodeExecutionInput")
Output AgentNodeExecutionInputOutput[] @relation("AgentNodeExecutionOutput")
@@ -627,9 +623,9 @@ model StoreListing {
ActiveVersion StoreListingVersion? @relation("ActiveVersion", fields: [activeVersionId], references: [id])
// The agent link here is only so we can do lookup on agentId
agentId String
agentVersion Int
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
agentGraphId String
agentGraphVersion Int
AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version], onDelete: Cascade)
owningUserId String
OwningUser User @relation(fields: [owningUserId], references: [id])
@@ -638,7 +634,7 @@ model StoreListing {
Versions StoreListingVersion[] @relation("ListingVersions")
// Unique index on agentId to ensure only one listing per agent, regardless of number of versions the agent has.
@@unique([agentId])
@@unique([agentGraphId])
@@unique([owningUserId, slug])
// Used in the view query
@@index([isDeleted, hasApprovedVersion])
@@ -651,9 +647,9 @@ model StoreListingVersion {
updatedAt DateTime @default(now()) @updatedAt
// The agent and version to be listed on the store
agentId String
agentVersion Int
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version])
agentGraphId String
agentGraphVersion Int
AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version])
// Content fields
name String
@@ -697,7 +693,7 @@ model StoreListingVersion {
@@index([storeListingId, submissionStatus, isAvailable])
@@index([submissionStatus])
@@index([reviewerId])
@@index([agentId, agentVersion]) // Non-unique index for efficient lookups
@@index([agentGraphId, agentGraphVersion]) // Non-unique index for efficient lookups
}
model StoreListingReview {

View File

@@ -360,8 +360,8 @@ async def test_execute_preset(server: SpinTestServer):
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,
graph_id=test_graph.id,
graph_version=test_graph.version,
inputs={
"dictionary": {"key1": "Hello", "key2": "World"},
"selected_value": "key2",
@@ -449,8 +449,8 @@ async def test_execute_preset_with_clash(server: SpinTestServer):
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,
graph_id=test_graph.id,
graph_version=test_graph.version,
inputs={
"dictionary": {"key1": "Hello", "key2": "World"},
"selected_value": "key2",

View File

@@ -82,16 +82,16 @@ export default function AgentRunsPage(): React.ReactElement {
api.getLibraryAgent(agentID).then((agent) => {
setAgent(agent);
getGraphVersion(agent.agent_id, agent.agent_version).then(
getGraphVersion(agent.graph_id, agent.graph_version).then(
(_graph) =>
(graph && graph.version == _graph.version) || setGraph(_graph),
);
api.getGraphExecutions(agent.agent_id).then((agentRuns) => {
api.getGraphExecutions(agent.graph_id).then((agentRuns) => {
setAgentRuns(agentRuns);
// Preload the corresponding graph versions
new Set(agentRuns.map((run) => run.graph_version)).forEach((version) =>
getGraphVersion(agent.agent_id, version),
getGraphVersion(agent.graph_id, version),
);
if (!selectedView.id && isFirstLoad && agentRuns.length > 0) {
@@ -109,7 +109,7 @@ export default function AgentRunsPage(): React.ReactElement {
});
if (selectedView.type == "run" && selectedView.id && agent) {
api
.getGraphExecutionInfo(agent.agent_id, selectedView.id)
.getGraphExecutionInfo(agent.graph_id, selectedView.id)
.then(setSelectedRun);
}
}, [api, agentID, getGraphVersion, graph, selectedView, isFirstLoad, agent]);
@@ -123,7 +123,7 @@ export default function AgentRunsPage(): React.ReactElement {
if (!agent) return;
// Subscribe to all executions for this agent
api.subscribeToGraphExecutions(agent.agent_id);
api.subscribeToGraphExecutions(agent.graph_id);
}, [api, agent]);
// Handle execution updates
@@ -162,7 +162,7 @@ export default function AgentRunsPage(): React.ReactElement {
// Ensure corresponding graph version is available before rendering I/O
api
.getGraphExecutionInfo(agent.agent_id, selectedView.id)
.getGraphExecutionInfo(agent.graph_id, selectedView.id)
.then(async (run) => {
await getGraphVersion(run.graph_id, run.graph_version);
setSelectedRun(run);
@@ -175,7 +175,7 @@ export default function AgentRunsPage(): React.ReactElement {
// TODO: filter in backend - https://github.com/Significant-Gravitas/AutoGPT/issues/9183
setSchedules(
(await api.listSchedules()).filter((s) => s.graph_id == agent.agent_id),
(await api.listSchedules()).filter((s) => s.graph_id == agent.graph_id),
);
}, [api, agent]);
@@ -214,7 +214,7 @@ export default function AgentRunsPage(): React.ReactElement {
agent &&
// Export sanitized graph from backend
api
.getGraph(agent.agent_id, agent.agent_version, true)
.getGraph(agent.graph_id, agent.graph_version, true)
.then((graph) =>
exportAsJSONFile(graph, `${graph.name}_v${graph.version}.json`),
),
@@ -227,7 +227,7 @@ export default function AgentRunsPage(): React.ReactElement {
? [
{
label: "Open graph in builder",
href: `/build?flowID=${agent.agent_id}&flowVersion=${agent.agent_version}`,
href: `/build?flowID=${agent.graph_id}&flowVersion=${agent.graph_version}`,
},
{ label: "Export agent to file", callback: downloadGraph },
]

View File

@@ -98,7 +98,7 @@ const Monitor = () => {
flows={flows}
executions={[
...(selectedFlow
? executions.filter((v) => v.graph_id == selectedFlow.agent_id)
? executions.filter((v) => v.graph_id == selectedFlow.graph_id)
: executions),
].sort((a, b) => b.started_at.getTime() - a.started_at.getTime())}
selectedRun={selectedRun}
@@ -108,7 +108,7 @@ const Monitor = () => {
<FlowRunInfo
agent={
selectedFlow ||
flows.find((f) => f.agent_id == selectedRun.graph_id)!
flows.find((f) => f.graph_id == selectedRun.graph_id)!
}
execution={selectedRun}
className={column3}
@@ -118,7 +118,7 @@ const Monitor = () => {
<FlowInfo
flow={selectedFlow}
executions={executions.filter(
(e) => e.graph_id == selectedFlow.agent_id,
(e) => e.graph_id == selectedFlow.graph_id,
)}
className={column3}
refresh={() => {

View File

@@ -84,7 +84,7 @@ export default function AgentRunsSelectorList({
>
<span>Scheduled</span>
<span className="text-neutral-600">
{schedules.filter((s) => s.graph_id === agent.agent_id).length}
{schedules.filter((s) => s.graph_id === agent.graph_id).length}
</span>
</Badge>
</div>
@@ -127,7 +127,7 @@ export default function AgentRunsSelectorList({
/>
))
: schedules
.filter((schedule) => schedule.graph_id === agent.agent_id)
.filter((schedule) => schedule.graph_id === agent.graph_id)
.map((schedule) => (
<AgentRunSummaryCard
className="h-28 w-72 lg:h-32 xl:w-80"

View File

@@ -8,7 +8,7 @@ export default function LibraryAgentCard({
id,
name,
description,
agent_id,
graph_id: agent_id,
can_access_graph,
creator_image_url,
image_url,

View File

@@ -109,7 +109,7 @@ export const AgentFlowList = ({
lastRun: GraphExecutionMeta | null = null;
if (executions) {
const _flowRuns = executions.filter(
(r) => r.graph_id == flow.agent_id,
(r) => r.graph_id == flow.graph_id,
);
runCount = _flowRuns.length;
lastRun =

View File

@@ -49,7 +49,7 @@ export const FlowInfo: React.FC<
}
> = ({ flow, executions, flowVersion, refresh, ...props }) => {
const { requestSaveAndRun, requestStopRun, isRunning, nodes, setNodes } =
useAgentGraph(flow.agent_id, flow.agent_version, undefined, false);
useAgentGraph(flow.graph_id, flow.graph_version, undefined, false);
const api = useBackendAPI();
const { toast } = useToast();
@@ -61,7 +61,7 @@ export const FlowInfo: React.FC<
const selectedFlowVersion: Graph | undefined = flowVersions?.find(
(v) =>
v.version ==
(selectedVersion == "all" ? flow.agent_version : selectedVersion),
(selectedVersion == "all" ? flow.graph_version : selectedVersion),
);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@@ -110,9 +110,9 @@ export const FlowInfo: React.FC<
useEffect(() => {
api
.getGraphAllVersions(flow.agent_id)
.getGraphAllVersions(flow.graph_id)
.then((result) => setFlowVersions(result));
}, [flow.agent_id, api]);
}, [flow.graph_id, api]);
const openRunnerInput = () => setIsRunnerInputOpen(true);
@@ -152,7 +152,7 @@ export const FlowInfo: React.FC<
<Card {...props}>
<CardHeader className="">
<CardTitle>
{flow.name} <span className="font-light">v{flow.agent_version}</span>
{flow.name} <span className="font-light">v{flow.graph_version}</span>
</CardTitle>
<div className="flex flex-col space-y-2 py-6">
{(flowVersions?.length ?? 0) > 1 && (
@@ -195,7 +195,7 @@ export const FlowInfo: React.FC<
{flow.can_access_graph && (
<Link
className={buttonVariants({ variant: "default" })}
href={`/build?flowID=${flow.agent_id}&flowVersion=${flow.agent_version}`}
href={`/build?flowID=${flow.graph_id}&flowVersion=${flow.graph_version}`}
>
<Pencil2Icon className="mr-2" />
Open in Builder
@@ -209,7 +209,7 @@ export const FlowInfo: React.FC<
data-testid="export-button"
onClick={() =>
api
.getGraph(flow.agent_id, selectedFlowVersion!.version, true)
.getGraph(flow.graph_id, selectedFlowVersion!.version, true)
.then((graph) =>
exportAsJSONFile(
graph,
@@ -248,7 +248,7 @@ export const FlowInfo: React.FC<
flows={[flow]}
executions={executions.filter(
(execution) =>
execution.graph_id == flow.agent_id &&
execution.graph_id == flow.graph_id &&
(selectedVersion == "all" ||
execution.graph_version == selectedVersion),
)}

View File

@@ -26,9 +26,9 @@ export const FlowRunInfo: React.FC<
const api = useBackendAPI();
const fetchBlockResults = useCallback(async () => {
const graph = await api.getGraph(agent.agent_id, agent.agent_version);
const graph = await api.getGraph(agent.graph_id, agent.graph_version);
const graphExecution = await api.getGraphExecutionInfo(
agent.agent_id,
agent.graph_id,
execution.id,
);
@@ -49,7 +49,7 @@ export const FlowRunInfo: React.FC<
),
),
);
}, [api, agent.agent_id, agent.agent_version, execution.id]);
}, [api, agent.graph_id, agent.graph_version, execution.id]);
// Fetch graph and execution data
useEffect(() => {
@@ -57,15 +57,15 @@ export const FlowRunInfo: React.FC<
fetchBlockResults();
}, [isOutputOpen, fetchBlockResults]);
if (execution.graph_id != agent.agent_id) {
if (execution.graph_id != agent.graph_id) {
throw new Error(
`FlowRunInfo can't be used with non-matching execution.graph_id and flow.id`,
);
}
const handleStopRun = useCallback(() => {
api.stopGraphExecution(agent.agent_id, execution.id);
}, [api, agent.agent_id, execution.id]);
api.stopGraphExecution(agent.graph_id, execution.id);
}, [api, agent.graph_id, execution.id]);
return (
<>
@@ -98,7 +98,7 @@ export const FlowRunInfo: React.FC<
</CardHeader>
<CardContent>
<p className="hidden">
<strong>Agent ID:</strong> <code>{agent.agent_id}</code>
<strong>Agent ID:</strong> <code>{agent.graph_id}</code>
</p>
<p className="hidden">
<strong>Run ID:</strong> <code>{execution.id}</code>

View File

@@ -48,7 +48,7 @@ export const FlowRunsList: React.FC<{
<TableCell>
<TextRenderer
value={
flows.find((f) => f.agent_id == execution.graph_id)?.name
flows.find((f) => f.graph_id == execution.graph_id)?.name
}
truncateLengthLimit={30}
/>

View File

@@ -64,7 +64,7 @@ export const FlowRunsTimeline = ({
time: number;
_duration: number;
} = payload[0].payload;
const flow = flows.find((f) => f.agent_id === data.graph_id);
const flow = flows.find((f) => f.graph_id === data.graph_id);
return (
<Card className="p-2 text-xs leading-normal">
<p>
@@ -98,7 +98,7 @@ export const FlowRunsTimeline = ({
<Scatter
key={flow.id}
data={executions
.filter((e) => e.graph_id == flow.agent_id)
.filter((e) => e.graph_id == flow.graph_id)
.map((e) => ({
...e,
time:

View File

@@ -90,8 +90,8 @@ export const SchedulesTable = ({
const handleAgentSelect = (agentId: string) => {
setSelectedAgent(agentId);
const agent = agents.find((a) => a.id === agentId);
setMaxVersion(agent!.agent_version);
setSelectedVersion(agent!.agent_version);
setMaxVersion(agent!.graph_version);
setSelectedVersion(agent!.graph_version);
};
const handleVersionSelect = (version: string) => {
@@ -120,7 +120,7 @@ export const SchedulesTable = ({
try {
await new Promise((resolve) => setTimeout(resolve, 100));
router.push(
`/build?flowID=${agent.agent_id}&flowVersion=${agent.agent_version}&open_scheduling=true`,
`/build?flowID=${agent.graph_id}&flowVersion=${agent.graph_version}&open_scheduling=true`,
);
} catch (error) {
console.error("Navigation error:", error);
@@ -184,7 +184,7 @@ export const SchedulesTable = ({
</SelectTrigger>
<SelectContent className="text-xs">
{agents.map((agent) => (
<SelectItem key={agent.id} value={agent.agent_id}>
<SelectItem key={agent.id} value={agent.graph_id}>
{agent.name}
</SelectItem>
))}
@@ -237,7 +237,7 @@ export const SchedulesTable = ({
filteredAndSortedSchedules.map((schedule) => (
<TableRow key={schedule.id}>
<TableCell className="font-medium">
{agents.find((a) => a.agent_id === schedule.graph_id)
{agents.find((a) => a.graph_id === schedule.graph_id)
?.name || schedule.graph_id}
</TableCell>
<TableCell>{schedule.graph_version}</TableCell>

View File

@@ -356,8 +356,8 @@ export type NodeExecutionResult = {
/* Mirror of backend/server/v2/library/model.py:LibraryAgent */
export type LibraryAgent = {
id: LibraryAgentID;
agent_id: GraphID;
agent_version: number;
graph_id: GraphID;
graph_version: number;
image_url?: string;
creator_name: string;
creator_image_url: string;
@@ -393,8 +393,8 @@ export interface LibraryAgentResponse {
export interface LibraryAgentPreset {
id: string;
updated_at: Date;
agent_id: string;
agent_version: number;
graph_id: GraphID;
graph_version: number;
name: string;
description: string;
is_active: boolean;
@@ -414,8 +414,8 @@ export interface CreateLibraryAgentPresetRequest {
name: string;
description: string;
inputs: { [key: string]: any };
agent_id: string;
agent_version: number;
graph_id: GraphID;
graph_version: number;
is_active: boolean;
}