mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Compare commits
19 Commits
fix/copilo
...
feature/so
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64689f7319 | ||
|
|
095821b252 | ||
|
|
5cfdce995d | ||
|
|
c24ce05970 | ||
|
|
3f2cfa93ef | ||
|
|
fb035ffd35 | ||
|
|
68eb8f35d7 | ||
|
|
23b90151fe | ||
|
|
1ace125395 | ||
|
|
59b5e64c29 | ||
|
|
22666670cc | ||
|
|
9adaeda70c | ||
|
|
9a5a852be2 | ||
|
|
cce1c60ab7 | ||
|
|
34690c463d | ||
|
|
5feab3dcfa | ||
|
|
21631a565b | ||
|
|
2891e5c48e | ||
|
|
05d1269758 |
@@ -22,6 +22,8 @@ from backend.data.graph import GraphSettings
|
||||
from backend.data.includes import (
|
||||
AGENT_PRESET_INCLUDE,
|
||||
LIBRARY_FOLDER_INCLUDE,
|
||||
MAX_LIBRARY_AGENT_EXECUTIONS_FETCH,
|
||||
MAX_LIBRARY_AGENTS_LAST_EXECUTED_FETCH,
|
||||
library_agent_include,
|
||||
)
|
||||
from backend.data.model import CredentialsMetaInput, GraphInput
|
||||
@@ -59,7 +61,7 @@ async def list_library_agents(
|
||||
Args:
|
||||
user_id: The ID of the user whose LibraryAgents we want to retrieve.
|
||||
search_term: Optional string to filter agents by name/description.
|
||||
sort_by: Sorting field (createdAt, updatedAt, isFavorite, isCreatedByUser).
|
||||
sort_by: Sorting field (createdAt, updatedAt, lastExecuted).
|
||||
page: Current page (1-indexed).
|
||||
page_size: Number of items per page.
|
||||
folder_id: Filter by folder ID. If provided, only returns agents in this folder.
|
||||
@@ -124,16 +126,84 @@ async def list_library_agents(
|
||||
elif sort_by == library_model.LibraryAgentSort.UPDATED_AT:
|
||||
order_by = {"updatedAt": "desc"}
|
||||
|
||||
library_agents = await prisma.models.LibraryAgent.prisma().find_many(
|
||||
where=where_clause,
|
||||
include=library_agent_include(
|
||||
user_id, include_nodes=False, include_executions=include_executions
|
||||
),
|
||||
order=order_by,
|
||||
skip=(page - 1) * page_size,
|
||||
take=page_size,
|
||||
)
|
||||
agent_count = await prisma.models.LibraryAgent.prisma().count(where=where_clause)
|
||||
# For LAST_EXECUTED sorting, we need to fetch execution data and sort in Python
|
||||
# since Prisma doesn't support sorting by nested relations
|
||||
if sort_by == library_model.LibraryAgentSort.LAST_EXECUTED:
|
||||
# TODO: This fetches up to MAX_LIBRARY_AGENTS_LAST_EXECUTED_FETCH agents
|
||||
# into memory for sorting. Prisma doesn't support sorting by nested relations,
|
||||
# so a dedicated lastExecutedAt column or raw SQL query would be needed for
|
||||
# database-level pagination. The ceiling prevents worst-case memory blowup.
|
||||
library_agents = await prisma.models.LibraryAgent.prisma().find_many(
|
||||
where=where_clause,
|
||||
take=MAX_LIBRARY_AGENTS_LAST_EXECUTED_FETCH,
|
||||
include=library_agent_include(
|
||||
user_id,
|
||||
include_nodes=False,
|
||||
include_executions=True,
|
||||
execution_limit=1,
|
||||
),
|
||||
)
|
||||
|
||||
def get_sort_key(
|
||||
agent: prisma.models.LibraryAgent,
|
||||
) -> tuple[int, float]:
|
||||
"""
|
||||
Returns a tuple for sorting: (has_no_executions, -timestamp).
|
||||
|
||||
Agents WITH executions come first (sorted by most recent execution),
|
||||
agents WITHOUT executions come last (sorted by creation date).
|
||||
"""
|
||||
graph = agent.AgentGraph
|
||||
if graph and graph.Executions and len(graph.Executions) > 0:
|
||||
execution = graph.Executions[0]
|
||||
timestamp = execution.updatedAt or execution.createdAt
|
||||
return (0, -timestamp.timestamp())
|
||||
return (1, -agent.createdAt.timestamp())
|
||||
|
||||
library_agents.sort(key=get_sort_key)
|
||||
|
||||
# Apply pagination after sorting
|
||||
agent_count = len(library_agents)
|
||||
start_idx = (page - 1) * page_size
|
||||
end_idx = start_idx + page_size
|
||||
page_agents = library_agents[start_idx:end_idx]
|
||||
|
||||
# Re-fetch the page agents with full execution data so that metrics
|
||||
# (execution_count, success_rate, avg_correctness_score, status) are
|
||||
# accurate. The sort-only fetch above used execution_limit=1 which
|
||||
# would make all metrics derived from a single execution.
|
||||
if include_executions and page_agents:
|
||||
page_agent_ids = [a.id for a in page_agents]
|
||||
full_exec_agents = await prisma.models.LibraryAgent.prisma().find_many(
|
||||
where={"id": {"in": page_agent_ids}},
|
||||
include=library_agent_include(
|
||||
user_id,
|
||||
include_nodes=False,
|
||||
include_executions=True,
|
||||
execution_limit=MAX_LIBRARY_AGENT_EXECUTIONS_FETCH,
|
||||
),
|
||||
)
|
||||
# Restore sort order (find_many with `in` does not guarantee order)
|
||||
full_exec_map = {a.id: a for a in full_exec_agents}
|
||||
library_agents = [
|
||||
full_exec_map[a.id] for a in page_agents if a.id in full_exec_map
|
||||
]
|
||||
else:
|
||||
library_agents = page_agents
|
||||
else:
|
||||
# Standard sorting via database
|
||||
library_agents = await prisma.models.LibraryAgent.prisma().find_many(
|
||||
where=where_clause,
|
||||
include=library_agent_include(
|
||||
user_id, include_nodes=False, include_executions=include_executions
|
||||
),
|
||||
order=order_by,
|
||||
skip=(page - 1) * page_size,
|
||||
take=page_size,
|
||||
)
|
||||
agent_count = await prisma.models.LibraryAgent.prisma().count(
|
||||
where=where_clause
|
||||
)
|
||||
|
||||
logger.debug(f"Retrieved {len(library_agents)} library agents for user #{user_id}")
|
||||
|
||||
@@ -337,6 +407,20 @@ async def get_library_agent_by_graph_id(
|
||||
graph_id: str,
|
||||
graph_version: Optional[int] = None,
|
||||
) -> library_model.LibraryAgent | None:
|
||||
"""
|
||||
Retrieves a library agent by its graph ID for a given user.
|
||||
|
||||
Args:
|
||||
user_id: The ID of the user who owns the library agent.
|
||||
graph_id: The ID of the agent graph to look up.
|
||||
graph_version: Optional specific version of the graph to retrieve.
|
||||
|
||||
Returns:
|
||||
The LibraryAgent if found, otherwise None.
|
||||
|
||||
Raises:
|
||||
DatabaseError: If there's an error during retrieval.
|
||||
"""
|
||||
filter: prisma.types.LibraryAgentWhereInput = {
|
||||
"agentGraphId": graph_id,
|
||||
"userId": user_id,
|
||||
@@ -724,6 +808,17 @@ async def update_library_agent(
|
||||
async def delete_library_agent(
|
||||
library_agent_id: str, user_id: str, soft_delete: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Deletes a library agent and cleans up associated schedules and webhooks.
|
||||
|
||||
Args:
|
||||
library_agent_id: The ID of the library agent to delete.
|
||||
user_id: The ID of the user who owns the library agent.
|
||||
soft_delete: If True, marks the agent as deleted; if False, permanently removes it.
|
||||
|
||||
Raises:
|
||||
NotFoundError: If the library agent is not found or doesn't belong to the user.
|
||||
"""
|
||||
# First get the agent to find the graph_id for cleanup
|
||||
library_agent = await prisma.models.LibraryAgent.prisma().find_unique(
|
||||
where={"id": library_agent_id}, include={"AgentGraph": True}
|
||||
@@ -1827,6 +1922,20 @@ async def update_preset(
|
||||
async def set_preset_webhook(
|
||||
user_id: str, preset_id: str, webhook_id: str | None
|
||||
) -> library_model.LibraryAgentPreset:
|
||||
"""
|
||||
Sets or removes a webhook connection for a preset.
|
||||
|
||||
Args:
|
||||
user_id: The ID of the user who owns the preset.
|
||||
preset_id: The ID of the preset to update.
|
||||
webhook_id: The ID of the webhook to connect, or None to disconnect.
|
||||
|
||||
Returns:
|
||||
The updated LibraryAgentPreset.
|
||||
|
||||
Raises:
|
||||
NotFoundError: If the preset is not found or doesn't belong to the user.
|
||||
"""
|
||||
current = await prisma.models.AgentPreset.prisma().find_unique(
|
||||
where={"id": preset_id},
|
||||
include=AGENT_PRESET_INCLUDE,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import prisma.enums
|
||||
import prisma.models
|
||||
@@ -8,6 +8,7 @@ from backend.data.db import connect
|
||||
from backend.data.includes import library_agent_include
|
||||
|
||||
from . import db
|
||||
from . import model as library_model
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -224,3 +225,506 @@ async def test_add_agent_to_library_not_found(mocker):
|
||||
mock_store_listing_version.return_value.find_unique.assert_called_once_with(
|
||||
where={"id": "version123"}, include={"AgentGraph": True}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_library_agents_sort_by_last_executed(mocker):
|
||||
"""
|
||||
Test LAST_EXECUTED sorting behavior:
|
||||
- Agents WITH executions come first, sorted by most recent execution (updatedAt)
|
||||
- Agents WITHOUT executions come last, sorted by creation date
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Agent 1: Has execution that finished 1 hour ago
|
||||
agent1_execution = prisma.models.AgentGraphExecution(
|
||||
id="exec1",
|
||||
agentGraphId="agent1",
|
||||
agentGraphVersion=1,
|
||||
userId="test-user",
|
||||
createdAt=now - timedelta(hours=2),
|
||||
updatedAt=now - timedelta(hours=1), # Finished 1 hour ago
|
||||
executionStatus=prisma.enums.AgentExecutionStatus.COMPLETED,
|
||||
isDeleted=False,
|
||||
isShared=False,
|
||||
)
|
||||
agent1_graph = prisma.models.AgentGraph(
|
||||
id="agent1",
|
||||
version=1,
|
||||
name="Agent With Recent Execution",
|
||||
description="Has execution finished 1 hour ago",
|
||||
userId="test-user",
|
||||
isActive=True,
|
||||
createdAt=now - timedelta(days=5),
|
||||
Executions=[agent1_execution],
|
||||
)
|
||||
library_agent1 = prisma.models.LibraryAgent(
|
||||
id="lib1",
|
||||
userId="test-user",
|
||||
agentGraphId="agent1",
|
||||
agentGraphVersion=1,
|
||||
settings="{}", # type: ignore
|
||||
isCreatedByUser=True,
|
||||
isDeleted=False,
|
||||
isArchived=False,
|
||||
createdAt=now - timedelta(days=5),
|
||||
updatedAt=now - timedelta(days=5),
|
||||
isFavorite=False,
|
||||
useGraphIsActiveVersion=True,
|
||||
AgentGraph=agent1_graph,
|
||||
)
|
||||
|
||||
# Agent 2: Has execution that finished 3 hours ago
|
||||
agent2_execution = prisma.models.AgentGraphExecution(
|
||||
id="exec2",
|
||||
agentGraphId="agent2",
|
||||
agentGraphVersion=1,
|
||||
userId="test-user",
|
||||
createdAt=now - timedelta(hours=5),
|
||||
updatedAt=now - timedelta(hours=3), # Finished 3 hours ago
|
||||
executionStatus=prisma.enums.AgentExecutionStatus.COMPLETED,
|
||||
isDeleted=False,
|
||||
isShared=False,
|
||||
)
|
||||
agent2_graph = prisma.models.AgentGraph(
|
||||
id="agent2",
|
||||
version=1,
|
||||
name="Agent With Older Execution",
|
||||
description="Has execution finished 3 hours ago",
|
||||
userId="test-user",
|
||||
isActive=True,
|
||||
createdAt=now - timedelta(days=3),
|
||||
Executions=[agent2_execution],
|
||||
)
|
||||
library_agent2 = prisma.models.LibraryAgent(
|
||||
id="lib2",
|
||||
userId="test-user",
|
||||
agentGraphId="agent2",
|
||||
agentGraphVersion=1,
|
||||
settings="{}", # type: ignore
|
||||
isCreatedByUser=True,
|
||||
isDeleted=False,
|
||||
isArchived=False,
|
||||
createdAt=now - timedelta(days=3),
|
||||
updatedAt=now - timedelta(days=3),
|
||||
isFavorite=False,
|
||||
useGraphIsActiveVersion=True,
|
||||
AgentGraph=agent2_graph,
|
||||
)
|
||||
|
||||
# Agent 3: No executions, created 1 day ago (should come after agents with executions)
|
||||
agent3_graph = prisma.models.AgentGraph(
|
||||
id="agent3",
|
||||
version=1,
|
||||
name="Agent Without Executions (Newer)",
|
||||
description="No executions, created 1 day ago",
|
||||
userId="test-user",
|
||||
isActive=True,
|
||||
createdAt=now - timedelta(days=1),
|
||||
Executions=[],
|
||||
)
|
||||
library_agent3 = prisma.models.LibraryAgent(
|
||||
id="lib3",
|
||||
userId="test-user",
|
||||
agentGraphId="agent3",
|
||||
agentGraphVersion=1,
|
||||
settings="{}", # type: ignore
|
||||
isCreatedByUser=True,
|
||||
isDeleted=False,
|
||||
isArchived=False,
|
||||
createdAt=now - timedelta(days=1),
|
||||
updatedAt=now - timedelta(days=1),
|
||||
isFavorite=False,
|
||||
useGraphIsActiveVersion=True,
|
||||
AgentGraph=agent3_graph,
|
||||
)
|
||||
|
||||
# Agent 4: No executions, created 2 days ago
|
||||
agent4_graph = prisma.models.AgentGraph(
|
||||
id="agent4",
|
||||
version=1,
|
||||
name="Agent Without Executions (Older)",
|
||||
description="No executions, created 2 days ago",
|
||||
userId="test-user",
|
||||
isActive=True,
|
||||
createdAt=now - timedelta(days=2),
|
||||
Executions=[],
|
||||
)
|
||||
library_agent4 = prisma.models.LibraryAgent(
|
||||
id="lib4",
|
||||
userId="test-user",
|
||||
agentGraphId="agent4",
|
||||
agentGraphVersion=1,
|
||||
settings="{}", # type: ignore
|
||||
isCreatedByUser=True,
|
||||
isDeleted=False,
|
||||
isArchived=False,
|
||||
createdAt=now - timedelta(days=2),
|
||||
updatedAt=now - timedelta(days=2),
|
||||
isFavorite=False,
|
||||
useGraphIsActiveVersion=True,
|
||||
AgentGraph=agent4_graph,
|
||||
)
|
||||
|
||||
# Return agents in random order to verify sorting works
|
||||
mock_library_agents = [
|
||||
library_agent3,
|
||||
library_agent1,
|
||||
library_agent4,
|
||||
library_agent2,
|
||||
]
|
||||
|
||||
# Mock prisma calls
|
||||
mock_agent_graph = mocker.patch("prisma.models.AgentGraph.prisma")
|
||||
mock_agent_graph.return_value.find_many = mocker.AsyncMock(return_value=[])
|
||||
|
||||
mock_library_agent = mocker.patch("prisma.models.LibraryAgent.prisma")
|
||||
mock_library_agent.return_value.find_many = mocker.AsyncMock(
|
||||
return_value=mock_library_agents
|
||||
)
|
||||
|
||||
# Call function with LAST_EXECUTED sort (without include_executions)
|
||||
result = await db.list_library_agents(
|
||||
"test-user",
|
||||
sort_by=library_model.LibraryAgentSort.LAST_EXECUTED,
|
||||
)
|
||||
|
||||
# Verify sorting order:
|
||||
# 1. Agent 1 (execution finished 1 hour ago) - most recent execution
|
||||
# 2. Agent 2 (execution finished 3 hours ago) - older execution
|
||||
# 3. Agent 3 (no executions, created 1 day ago) - newer creation
|
||||
# 4. Agent 4 (no executions, created 2 days ago) - older creation
|
||||
assert len(result.agents) == 4
|
||||
assert (
|
||||
result.agents[0].id == "lib1"
|
||||
), "Agent with most recent execution should be first"
|
||||
assert result.agents[1].id == "lib2", "Agent with older execution should be second"
|
||||
assert (
|
||||
result.agents[2].id == "lib3"
|
||||
), "Agent without executions (newer) should be third"
|
||||
assert (
|
||||
result.agents[3].id == "lib4"
|
||||
), "Agent without executions (older) should be last"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_library_agents_last_executed_metrics_accuracy(mocker):
|
||||
"""
|
||||
Test that when LAST_EXECUTED sort is used with include_executions=True,
|
||||
metrics (execution_count, success_rate) are computed from the full execution
|
||||
history, not from the single execution used for sort-order determination.
|
||||
|
||||
Bug: execution_limit=1 was used for both sorting AND metric calculation,
|
||||
causing execution_count to always be 0 or 1 and success_rate to be 0% or 100%.
|
||||
Fix: after sorting/pagination, re-fetch the page agents with full execution data.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Agent with 1 execution (used for sort-key fetch, execution_limit=1)
|
||||
sort_execution = prisma.models.AgentGraphExecution(
|
||||
id="exec-sort",
|
||||
agentGraphId="agent1",
|
||||
agentGraphVersion=1,
|
||||
userId="test-user",
|
||||
createdAt=now - timedelta(hours=2),
|
||||
updatedAt=now - timedelta(hours=1),
|
||||
executionStatus=prisma.enums.AgentExecutionStatus.COMPLETED,
|
||||
isDeleted=False,
|
||||
isShared=False,
|
||||
)
|
||||
sort_graph = prisma.models.AgentGraph(
|
||||
id="agent1",
|
||||
version=1,
|
||||
name="Agent With Many Executions",
|
||||
description="Should show full execution count",
|
||||
userId="test-user",
|
||||
isActive=True,
|
||||
createdAt=now - timedelta(days=5),
|
||||
Executions=[sort_execution], # Only 1 for sort
|
||||
)
|
||||
sort_library_agent = prisma.models.LibraryAgent(
|
||||
id="lib1",
|
||||
userId="test-user",
|
||||
agentGraphId="agent1",
|
||||
agentGraphVersion=1,
|
||||
settings="{}", # type: ignore
|
||||
isCreatedByUser=True,
|
||||
isDeleted=False,
|
||||
isArchived=False,
|
||||
createdAt=now - timedelta(days=5),
|
||||
updatedAt=now - timedelta(days=5),
|
||||
isFavorite=False,
|
||||
useGraphIsActiveVersion=True,
|
||||
AgentGraph=sort_graph,
|
||||
)
|
||||
|
||||
# Agent with full execution history (used for metric calculation, full execution_limit)
|
||||
full_exec1 = prisma.models.AgentGraphExecution(
|
||||
id="exec1",
|
||||
agentGraphId="agent1",
|
||||
agentGraphVersion=1,
|
||||
userId="test-user",
|
||||
createdAt=now - timedelta(hours=2),
|
||||
updatedAt=now - timedelta(hours=1),
|
||||
executionStatus=prisma.enums.AgentExecutionStatus.COMPLETED,
|
||||
isDeleted=False,
|
||||
isShared=False,
|
||||
)
|
||||
full_exec2 = prisma.models.AgentGraphExecution(
|
||||
id="exec2",
|
||||
agentGraphId="agent1",
|
||||
agentGraphVersion=1,
|
||||
userId="test-user",
|
||||
createdAt=now - timedelta(hours=4),
|
||||
updatedAt=now - timedelta(hours=3),
|
||||
executionStatus=prisma.enums.AgentExecutionStatus.FAILED,
|
||||
isDeleted=False,
|
||||
isShared=False,
|
||||
)
|
||||
full_exec3 = prisma.models.AgentGraphExecution(
|
||||
id="exec3",
|
||||
agentGraphId="agent1",
|
||||
agentGraphVersion=1,
|
||||
userId="test-user",
|
||||
createdAt=now - timedelta(hours=6),
|
||||
updatedAt=now - timedelta(hours=5),
|
||||
executionStatus=prisma.enums.AgentExecutionStatus.COMPLETED,
|
||||
isDeleted=False,
|
||||
isShared=False,
|
||||
)
|
||||
full_graph = prisma.models.AgentGraph(
|
||||
id="agent1",
|
||||
version=1,
|
||||
name="Agent With Many Executions",
|
||||
description="Should show full execution count",
|
||||
userId="test-user",
|
||||
isActive=True,
|
||||
createdAt=now - timedelta(days=5),
|
||||
Executions=[full_exec1, full_exec2, full_exec3], # All 3
|
||||
)
|
||||
full_library_agent = prisma.models.LibraryAgent(
|
||||
id="lib1",
|
||||
userId="test-user",
|
||||
agentGraphId="agent1",
|
||||
agentGraphVersion=1,
|
||||
settings="{}", # type: ignore
|
||||
isCreatedByUser=True,
|
||||
isDeleted=False,
|
||||
isArchived=False,
|
||||
createdAt=now - timedelta(days=5),
|
||||
updatedAt=now - timedelta(days=5),
|
||||
isFavorite=False,
|
||||
useGraphIsActiveVersion=True,
|
||||
AgentGraph=full_graph,
|
||||
)
|
||||
|
||||
mock_agent_graph = mocker.patch("prisma.models.AgentGraph.prisma")
|
||||
mock_agent_graph.return_value.find_many = mocker.AsyncMock(return_value=[])
|
||||
|
||||
mock_library_agent = mocker.patch("prisma.models.LibraryAgent.prisma")
|
||||
# First call: sort-key fetch (execution_limit=1) → returns sort_library_agent
|
||||
# Second call: full metric fetch → returns full_library_agent
|
||||
mock_library_agent.return_value.find_many = mocker.AsyncMock(
|
||||
side_effect=[
|
||||
[sort_library_agent],
|
||||
[full_library_agent],
|
||||
]
|
||||
)
|
||||
|
||||
result = await db.list_library_agents(
|
||||
"test-user",
|
||||
sort_by=library_model.LibraryAgentSort.LAST_EXECUTED,
|
||||
include_executions=True,
|
||||
)
|
||||
|
||||
assert len(result.agents) == 1
|
||||
agent = result.agents[0]
|
||||
assert agent.id == "lib1"
|
||||
# With the fix: metrics are computed from all 3 executions, not just 1
|
||||
assert agent.execution_count == 3, (
|
||||
"execution_count should reflect the full execution history, not the "
|
||||
"sort-key fetch which used execution_limit=1"
|
||||
)
|
||||
# 2 out of 3 executions are COMPLETED → 66.67%
|
||||
assert agent.success_rate is not None
|
||||
assert (
|
||||
abs(agent.success_rate - 200 / 3) < 0.01
|
||||
), "success_rate should be calculated from all executions"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_library_agents_last_executed_null_updated_at(mocker):
|
||||
"""
|
||||
Test that the LAST_EXECUTED sort gracefully handles executions where updatedAt
|
||||
is None — the sort key should fall back to createdAt instead.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
execution_no_updated = prisma.models.AgentGraphExecution(
|
||||
id="exec-no-updated",
|
||||
agentGraphId="agent1",
|
||||
agentGraphVersion=1,
|
||||
userId="test-user",
|
||||
createdAt=now - timedelta(hours=2),
|
||||
updatedAt=None,
|
||||
executionStatus=prisma.enums.AgentExecutionStatus.RUNNING,
|
||||
isDeleted=False,
|
||||
isShared=False,
|
||||
)
|
||||
graph1 = prisma.models.AgentGraph(
|
||||
id="agent1",
|
||||
version=1,
|
||||
name="Agent With Null UpdatedAt",
|
||||
description="",
|
||||
userId="test-user",
|
||||
isActive=True,
|
||||
createdAt=now - timedelta(days=1),
|
||||
Executions=[execution_no_updated],
|
||||
)
|
||||
library_agent1 = prisma.models.LibraryAgent(
|
||||
id="lib1",
|
||||
userId="test-user",
|
||||
agentGraphId="agent1",
|
||||
agentGraphVersion=1,
|
||||
settings="{}", # type: ignore
|
||||
isCreatedByUser=True,
|
||||
isDeleted=False,
|
||||
isArchived=False,
|
||||
createdAt=now - timedelta(days=1),
|
||||
updatedAt=now - timedelta(days=1),
|
||||
isFavorite=False,
|
||||
useGraphIsActiveVersion=True,
|
||||
AgentGraph=graph1,
|
||||
)
|
||||
|
||||
mock_library_agent = mocker.patch("prisma.models.LibraryAgent.prisma")
|
||||
mock_library_agent.return_value.find_many = mocker.AsyncMock(
|
||||
return_value=[library_agent1]
|
||||
)
|
||||
|
||||
result = await db.list_library_agents(
|
||||
"test-user",
|
||||
sort_by=library_model.LibraryAgentSort.LAST_EXECUTED,
|
||||
)
|
||||
|
||||
assert len(result.agents) == 1
|
||||
assert result.agents[0].id == "lib1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_library_agents_last_executed_none_agent_graph(mocker):
|
||||
"""
|
||||
Test that the LAST_EXECUTED sort safely handles agents where AgentGraph is None.
|
||||
Such agents should fall to the bottom (treated as no executions).
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
agent_no_graph = prisma.models.LibraryAgent(
|
||||
id="lib-no-graph",
|
||||
userId="test-user",
|
||||
agentGraphId="agent-gone",
|
||||
agentGraphVersion=1,
|
||||
settings="{}", # type: ignore
|
||||
isCreatedByUser=True,
|
||||
isDeleted=False,
|
||||
isArchived=False,
|
||||
createdAt=now - timedelta(days=1),
|
||||
updatedAt=now - timedelta(days=1),
|
||||
isFavorite=False,
|
||||
useGraphIsActiveVersion=True,
|
||||
AgentGraph=None,
|
||||
)
|
||||
|
||||
mock_library_agent = mocker.patch("prisma.models.LibraryAgent.prisma")
|
||||
mock_library_agent.return_value.find_many = mocker.AsyncMock(
|
||||
return_value=[agent_no_graph]
|
||||
)
|
||||
|
||||
result = await db.list_library_agents(
|
||||
"test-user",
|
||||
sort_by=library_model.LibraryAgentSort.LAST_EXECUTED,
|
||||
)
|
||||
|
||||
assert (
|
||||
len(result.agents) == 0
|
||||
), "Agent with no graph should be skipped (from_db will fail gracefully)"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_library_agents_last_executed_pagination(mocker):
|
||||
"""
|
||||
Test that LAST_EXECUTED sort correctly applies in-memory pagination:
|
||||
page 1 returns first page_size agents, page 2 returns the next batch,
|
||||
and agent_count reflects the total across all pages.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
def make_agent(agent_id: str, lib_id: str, hours_ago: int):
|
||||
execution = prisma.models.AgentGraphExecution(
|
||||
id=f"exec-{agent_id}",
|
||||
agentGraphId=agent_id,
|
||||
agentGraphVersion=1,
|
||||
userId="test-user",
|
||||
createdAt=now - timedelta(hours=hours_ago + 1),
|
||||
updatedAt=now - timedelta(hours=hours_ago),
|
||||
executionStatus=prisma.enums.AgentExecutionStatus.COMPLETED,
|
||||
isDeleted=False,
|
||||
isShared=False,
|
||||
)
|
||||
graph = prisma.models.AgentGraph(
|
||||
id=agent_id,
|
||||
version=1,
|
||||
name=f"Agent {agent_id}",
|
||||
description="",
|
||||
userId="test-user",
|
||||
isActive=True,
|
||||
createdAt=now - timedelta(days=3),
|
||||
Executions=[execution],
|
||||
)
|
||||
return prisma.models.LibraryAgent(
|
||||
id=lib_id,
|
||||
userId="test-user",
|
||||
agentGraphId=agent_id,
|
||||
agentGraphVersion=1,
|
||||
settings="{}", # type: ignore
|
||||
isCreatedByUser=True,
|
||||
isDeleted=False,
|
||||
isArchived=False,
|
||||
createdAt=now - timedelta(days=3),
|
||||
updatedAt=now - timedelta(days=3),
|
||||
isFavorite=False,
|
||||
useGraphIsActiveVersion=True,
|
||||
AgentGraph=graph,
|
||||
)
|
||||
|
||||
# 3 agents, ordered newest-first by execution time: lib1, lib2, lib3
|
||||
agents = [
|
||||
make_agent("a1", "lib1", hours_ago=1),
|
||||
make_agent("a2", "lib2", hours_ago=2),
|
||||
make_agent("a3", "lib3", hours_ago=3),
|
||||
]
|
||||
|
||||
mock_library_agent = mocker.patch("prisma.models.LibraryAgent.prisma")
|
||||
mock_library_agent.return_value.find_many = mocker.AsyncMock(return_value=agents)
|
||||
|
||||
result_page1 = await db.list_library_agents(
|
||||
"test-user",
|
||||
sort_by=library_model.LibraryAgentSort.LAST_EXECUTED,
|
||||
page=1,
|
||||
page_size=2,
|
||||
)
|
||||
result_page2 = await db.list_library_agents(
|
||||
"test-user",
|
||||
sort_by=library_model.LibraryAgentSort.LAST_EXECUTED,
|
||||
page=2,
|
||||
page_size=2,
|
||||
)
|
||||
|
||||
assert result_page1.pagination.total_items == 3
|
||||
assert result_page1.pagination.total_pages == 2
|
||||
assert len(result_page1.agents) == 2
|
||||
assert result_page1.agents[0].id == "lib1"
|
||||
assert result_page1.agents[1].id == "lib2"
|
||||
|
||||
assert len(result_page2.agents) == 1
|
||||
assert result_page2.agents[0].id == "lib3"
|
||||
|
||||
@@ -539,6 +539,7 @@ class LibraryAgentSort(str, Enum):
|
||||
|
||||
CREATED_AT = "createdAt"
|
||||
UPDATED_AT = "updatedAt"
|
||||
LAST_EXECUTED = "lastExecuted"
|
||||
|
||||
|
||||
class LibraryAgentUpdateRequest(pydantic.BaseModel):
|
||||
|
||||
@@ -30,6 +30,7 @@ EXECUTION_RESULT_INCLUDE: prisma.types.AgentNodeExecutionInclude = {
|
||||
|
||||
MAX_NODE_EXECUTIONS_FETCH = 1000
|
||||
MAX_LIBRARY_AGENT_EXECUTIONS_FETCH = 10
|
||||
MAX_LIBRARY_AGENTS_LAST_EXECUTED_FETCH = 1000
|
||||
|
||||
# Default limits for potentially large result sets
|
||||
MAX_CREDIT_REFUND_REQUESTS_FETCH = 100
|
||||
@@ -109,6 +110,8 @@ def library_agent_include(
|
||||
- Listing optimization (no nodes/executions): ~2s for 15 agents vs potential timeouts
|
||||
- Unlimited executions: varies by user (thousands of executions = timeouts)
|
||||
"""
|
||||
if not user_id:
|
||||
raise ValueError("user_id is required")
|
||||
result: prisma.types.LibraryAgentInclude = {
|
||||
"Creator": True, # Always needed for creator info
|
||||
"Folder": True, # Always needed for folder info
|
||||
@@ -126,7 +129,7 @@ def library_agent_include(
|
||||
if include_executions:
|
||||
agent_graph_include["Executions"] = {
|
||||
"where": {"userId": user_id},
|
||||
"order_by": {"createdAt": "desc"},
|
||||
"order_by": {"updatedAt": "desc"},
|
||||
"take": execution_limit,
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ export function LibrarySortMenu({ setLibrarySort }: Props) {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value={LibraryAgentSort.lastExecuted}>
|
||||
Last Executed
|
||||
</SelectItem>
|
||||
<SelectItem value={LibraryAgentSort.createdAt}>
|
||||
Creation Date
|
||||
</SelectItem>
|
||||
|
||||
@@ -11,6 +11,8 @@ export function useLibrarySortMenu({ setLibrarySort }: Props) {
|
||||
|
||||
const getSortLabel = (sort: LibraryAgentSort) => {
|
||||
switch (sort) {
|
||||
case LibraryAgentSort.lastExecuted:
|
||||
return "Last Executed";
|
||||
case LibraryAgentSort.createdAt:
|
||||
return "Creation Date";
|
||||
case LibraryAgentSort.updatedAt:
|
||||
|
||||
@@ -10499,7 +10499,7 @@
|
||||
},
|
||||
"LibraryAgentSort": {
|
||||
"type": "string",
|
||||
"enum": ["createdAt", "updatedAt"],
|
||||
"enum": ["createdAt", "updatedAt", "lastExecuted"],
|
||||
"title": "LibraryAgentSort",
|
||||
"description": "Possible sort options for sorting library agents."
|
||||
},
|
||||
|
||||
@@ -600,6 +600,7 @@ export type LibraryAgentPresetUpdatable = Partial<
|
||||
export enum LibraryAgentSortEnum {
|
||||
CREATED_AT = "createdAt",
|
||||
UPDATED_AT = "updatedAt",
|
||||
LAST_EXECUTED = "lastExecuted",
|
||||
}
|
||||
|
||||
/* *** CREDENTIALS *** */
|
||||
|
||||
@@ -85,7 +85,7 @@ export class LibraryPage extends BasePage {
|
||||
|
||||
async selectSortOption(
|
||||
page: Page,
|
||||
sortOption: "Creation Date" | "Last Modified",
|
||||
sortOption: "Last Executed" | "Creation Date" | "Last Modified",
|
||||
): Promise<void> {
|
||||
const { getRole } = getSelectors(page);
|
||||
await getRole("combobox").click();
|
||||
|
||||
Reference in New Issue
Block a user