Compare commits

...

19 Commits

Author SHA1 Message Date
Zamil Majdy
64689f7319 fix(backend): address review — user_id guard, agent ceiling, edge-case tests
- includes.py: add `if not user_id: raise ValueError` guard in
  library_agent_include() as required by data-access coding guidelines
- includes.py: remove trailing inline comment from order_by block
- includes.py: add MAX_LIBRARY_AGENTS_LAST_EXECUTED_FETCH = 1000 constant
- db.py: apply take=MAX_LIBRARY_AGENTS_LAST_EXECUTED_FETCH ceiling on the
  LAST_EXECUTED find_many to prevent unbounded in-memory fetch
- db_test.py: add edge-case tests —
    * updatedAt=None fallback (sort key uses createdAt)
    * AgentGraph=None (agent gracefully skipped)
    * pagination correctness (total_items, total_pages, page 2 slice)
2026-03-19 15:44:06 +07:00
Zamil Majdy
095821b252 fix(backend): re-fetch page agents with full execution data after LAST_EXECUTED sort
When sorting library agents by LAST_EXECUTED, the sort-key fetch uses
execution_limit=1 to determine order cheaply. However, LibraryAgent.from_db
computed execution_count, success_rate, avg_correctness_score, and agent status
from that same truncated list — causing every agent to show execution_count=1
and a wildly inaccurate success_rate (0% or 100%).

Fix: after sorting and slicing the page, if include_executions=True, re-fetch
the page agents by ID with the full MAX_LIBRARY_AGENT_EXECUTIONS_FETCH limit
and restore sort order before passing to from_db.

Also adds a unit test asserting that execution_count and success_rate are
accurate across multiple executions when include_executions=True.
2026-03-19 15:37:36 +07:00
Otto (AGPT)
5cfdce995d fix(frontend): align sort placeholder and default label with actual default (updatedAt)
The contributor reverted the default sort to updatedAt to fix E2E tests,
but the placeholder and default label still said 'Last Executed'.
This caused a UI mismatch where users saw 'Last Executed' but got
'Last Modified' sorting.
2026-03-19 00:44:26 +00:00
Otto (AGPT)
c24ce05970 fix(backend): pass include_executions param instead of hardcoded False
The else branch (non-LAST_EXECUTED sorts) was hardcoding
include_executions=False, ignoring the caller's parameter.
This broke callers like the agent generator that need execution
data with non-LAST_EXECUTED sorts.
2026-03-19 00:44:26 +00:00
Otto (AGPT)
3f2cfa93ef fix(backend): update docstring sort options to match LibraryAgentSort enum 2026-03-19 00:44:26 +00:00
Otto (AGPT)
fb035ffd35 fix(backend): remove orphaned try blocks causing SyntaxError
Remove two incomplete try: blocks (missing except/finally clauses)
in db.py that caused SyntaxError across all backend CI test jobs.
2026-03-19 00:44:26 +00:00
Medyan
68eb8f35d7 fix spelling mistake 2026-03-19 00:44:26 +00:00
Medyan
23b90151fe set default to updatedAt 2026-03-19 00:44:26 +00:00
Nick Tindle
1ace125395 Revert "feat: persist sort param in URL for bookmarkability"
This reverts commit c8a267f10d.
2026-03-19 00:44:26 +00:00
Nick Tindle
59b5e64c29 feat: persist sort param in URL for bookmarkability
- Re-added useEffect to ensure sort param is always in URL
- Updated to use lastExecuted as default
- Updated signin tests to expect ?sort=lastExecuted
2026-03-19 00:44:26 +00:00
Nick Tindle
22666670cc fix(tests): revert unnecessary signup test changes
The signup flow still ends at /marketplace via the test helper.
Only the signin tests needed updating since the sort param useEffect was removed.
2026-03-19 00:44:26 +00:00
Medyan
9adaeda70c fix E2E Test Failure 2026-03-19 00:44:26 +00:00
Medyan
9a5a852be2 fix E2E Test Failure 2026-03-19 00:44:26 +00:00
Medyan
cce1c60ab7 fix format using poetry 2026-03-19 00:44:25 +00:00
Medyan
34690c463d add test case for agents_sort_by_last_executed 2026-03-19 00:44:25 +00:00
Medyan
5feab3dcfa add comment 2026-03-19 00:44:25 +00:00
Medyan
21631a565b fix failed test cases 2026-03-19 00:44:25 +00:00
Medyan
2891e5c48e make lastExecuted default for sorting agents 2026-03-19 00:44:25 +00:00
Medyan
05d1269758 Sort by most recent execution time
rebase with dev branch

rebase
2026-03-19 00:44:25 +00:00
9 changed files with 638 additions and 15 deletions

View File

@@ -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,

View File

@@ -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"

View File

@@ -539,6 +539,7 @@ class LibraryAgentSort(str, Enum):
CREATED_AT = "createdAt"
UPDATED_AT = "updatedAt"
LAST_EXECUTED = "lastExecuted"
class LibraryAgentUpdateRequest(pydantic.BaseModel):

View File

@@ -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,
}

View File

@@ -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>

View File

@@ -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:

View File

@@ -10499,7 +10499,7 @@
},
"LibraryAgentSort": {
"type": "string",
"enum": ["createdAt", "updatedAt"],
"enum": ["createdAt", "updatedAt", "lastExecuted"],
"title": "LibraryAgentSort",
"description": "Possible sort options for sorting library agents."
},

View File

@@ -600,6 +600,7 @@ export type LibraryAgentPresetUpdatable = Partial<
export enum LibraryAgentSortEnum {
CREATED_AT = "createdAt",
UPDATED_AT = "updatedAt",
LAST_EXECUTED = "lastExecuted",
}
/* *** CREDENTIALS *** */

View File

@@ -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();