From 2ee1ff50dde28eb99ad5536ed9ab6ebab98b74a5 Mon Sep 17 00:00:00 2001 From: Shubham Naik Date: Wed, 26 Nov 2025 11:18:12 -0800 Subject: [PATCH] Shub/let 6339 add endpoint for counting non hidden agents [LET-6339] (#6406) * feat: add hidden agent count * feat: add hidden agent count * chore; hiddne agents --------- Co-authored-by: Shubham Naik --- fern/openapi.json | 45 ++++++ .../rest_api/routers/v1/internal_agents.py | 24 ++- tests/test_internal_agents_count.py | 150 ++++++++++++++++++ 3 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 tests/test_internal_agents_count.py diff --git a/fern/openapi.json b/fern/openapi.json index 0971c18f..bdb37426 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -10034,6 +10034,51 @@ } } }, + "/v1/_internal_agents/count": { + "get": { + "tags": ["_internal_agents"], + "summary": "Count Agents", + "description": "Get the total number of agents for a user, with option to exclude hidden agents.", + "operationId": "count_internal_agents", + "parameters": [ + { + "name": "exclude_hidden", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "If True, excludes hidden agents from the count. If False, includes all agents.", + "default": true, + "title": "Exclude Hidden" + }, + "description": "If True, excludes hidden agents from the count. If False, includes all agents." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "integer", + "title": "Response Count Internal Agents" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/v1/_internal_agents/{agent_id}/core-memory/blocks/{block_label}": { "patch": { "tags": ["_internal_agents"], diff --git a/letta/server/rest_api/routers/v1/internal_agents.py b/letta/server/rest_api/routers/v1/internal_agents.py index de067c27..f89c57d9 100644 --- a/letta/server/rest_api/routers/v1/internal_agents.py +++ b/letta/server/rest_api/routers/v1/internal_agents.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Body, Depends +from fastapi import APIRouter, Body, Depends, Query from letta.schemas.block import Block, BlockUpdate from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server @@ -8,6 +8,28 @@ from letta.validators import AgentId router = APIRouter(prefix="/_internal_agents", tags=["_internal_agents"]) +@router.get("/count", response_model=int, operation_id="count_internal_agents") +async def count_agents( + exclude_hidden: bool = Query(True, description="If True, excludes hidden agents from the count. If False, includes all agents."), + server: "SyncServer" = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +): + """ + Get the total number of agents for a user, with option to exclude hidden agents. + """ + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + + # When exclude_hidden=True, we want show_hidden_agents=False + # When exclude_hidden=False, we want show_hidden_agents=True + show_hidden_agents = not exclude_hidden + + # Always use count_agents_async to ensure proper filtering + return await server.agent_manager.count_agents_async( + actor=actor, + show_hidden_agents=show_hidden_agents, + ) + + @router.patch("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="modify_internal_core_memory_block") async def modify_block_for_agent( block_label: str, diff --git a/tests/test_internal_agents_count.py b/tests/test_internal_agents_count.py new file mode 100644 index 00000000..534296a0 --- /dev/null +++ b/tests/test_internal_agents_count.py @@ -0,0 +1,150 @@ +import os +from typing import List + +import httpx +import pytest +from letta_client import Letta + +from letta.schemas.agent import AgentState + + +@pytest.fixture(scope="function") +def test_agents(client: Letta) -> List[AgentState]: + """ + Creates test agents - some hidden, some not hidden. + Cleans them up after the test. + """ + agents = [] + + # Create 3 non-hidden agents + for i in range(3): + agent = client.agents.create( + name=f"test_agent_visible_{i}", + tags=["test", "visible"], + model="openai/gpt-4o-mini", + embedding="openai/text-embedding-3-small", + ) + agents.append(agent) + + # Create 2 hidden agents + for i in range(2): + # Create agent as hidden using direct HTTP call (SDK might not support hidden parameter yet) + response = httpx.post( + f"{client._client._base_url}/v1/agents/", + json={ + "name": f"test_agent_hidden_{i}", + "tags": ["test", "hidden"], + "model": "openai/gpt-4o-mini", + "embedding": "openai/text-embedding-3-small", + "hidden": True, + }, + headers=client._client._headers, + timeout=10.0, + ) + response.raise_for_status() + agent_data = response.json() + + # Create a simple AgentState-like object for tracking + class SimpleAgent: + def __init__(self, id): + self.id = id + + agents.append(SimpleAgent(agent_data["id"])) + + yield agents + + # Cleanup + for agent in agents: + try: + client.agents.delete(agent.id) + except: + pass + + +def test_internal_agents_count_exclude_hidden(client: Letta, test_agents: List[AgentState]): + """ + Test that the internal agents count endpoint correctly excludes hidden agents + when exclude_hidden=True (default). + """ + # Make a request to the internal endpoint + # Note: We need to use the raw HTTP client since the SDK might not have this endpoint + response = httpx.get( + f"{client._client._base_url}/v1/_internal_agents/count", + params={"exclude_hidden": True}, + headers=client._client._headers, + timeout=10.0, + ) + + assert response.status_code == 200 + count = response.json() + + # Should count at least the 3 visible agents we created + # (there might be other agents in the system) + assert isinstance(count, int) + assert count >= 3 + + # Get the total count with hidden agents included + response_with_hidden = httpx.get( + f"{client._client._base_url}/v1/_internal_agents/count", + params={"exclude_hidden": False}, + headers=client._client._headers, + timeout=10.0, + ) + + assert response_with_hidden.status_code == 200 + count_with_hidden = response_with_hidden.json() + + # The count with hidden should be at least 2 more than without hidden + assert count_with_hidden >= count + 2 + + +def test_internal_agents_count_include_all(client: Letta, test_agents: List[AgentState]): + """ + Test that the internal agents count endpoint correctly includes all agents + when exclude_hidden=False. + """ + response = httpx.get( + f"{client._client._base_url}/v1/_internal_agents/count", + params={"exclude_hidden": False}, + headers=client._client._headers, + timeout=10.0, + ) + + assert response.status_code == 200 + count = response.json() + + # Should count at least all 5 agents we created (3 visible + 2 hidden) + assert isinstance(count, int) + assert count >= 5 + + +def test_internal_agents_count_default_behavior(client: Letta, test_agents: List[AgentState]): + """ + Test that the default behavior (exclude_hidden=True) works correctly. + """ + # Call without specifying exclude_hidden (should default to True) + response = httpx.get( + f"{client._client._base_url}/v1/_internal_agents/count", + headers=client._client._headers, + timeout=10.0, + ) + + assert response.status_code == 200 + count = response.json() + + # Should count at least the 3 visible agents we created + assert isinstance(count, int) + assert count >= 3 + + # This should be the same as explicitly setting exclude_hidden=True + response_explicit = httpx.get( + f"{client._client._base_url}/v1/_internal_agents/count", + params={"exclude_hidden": True}, + headers=client._client._headers, + timeout=10.0, + ) + + count_explicit = response_explicit.json() + + # The two counts should be equal + assert count == count_explicit