feat(web): add list-apps-detailed endpoint

Merge https://github.com/google/adk-python/pull/3430

**Please ensure you have read the [contribution guide](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) before creating a pull request.**

### Link to Issue or Description of Change

**1. Link to an existing issue (if applicable):**

- Closes: #3429

**2. Or, if no issue exists, describe the change:**

_If applicable, please follow the issue templates to provide as much detail as
possible._

**Problem:**
The existing `/list-apps` endpoint only returns the name of the folder that each agent is in

**Solution:**
This adds a new endpoint `/list-apps-detailed` which will load each agent using the existing `AgentLoader.load_agent` method, and then return the folder name, display name (with underscores replaced with spaces for a more readable version), description, and the agent type.

This does introduce overhead if you had multiple agents since they all need to be loaded, but by maintaining the existing `/list-apps` endpoint, users can choose which one to hit if they don't want to load all agents. Since the existing `load_agents` method will cache results, there's only a penalty on the first hit.

### Testing Plan

Created a unit test for this, similar to the `/list-apps`. Also tested this with my own ADK instance to verify it loaded correctly.
```
curl --location "localhost:8000/list-apps-detailed"
```
```json
{
    "apps": [
        {
            "name": "agent_1",
            "displayName": "Agent 1",
            "description": "A test description for a test agent",
            "agentType": "package"
        },
        {
            "name": "agent_2",
            "displayName": "Agent 2",
            "description": "A test description for a test agent ",
            "agentType": "package"
        },
        {
            "name": "agent_3",
            "displayName": "Agent 3",
            "description": "A test description for a test agent",
            "agentType": "package"
        }
    ]
}

```

**Unit Tests:**

- [X] I have added or updated unit tests for my change.
- [X] All unit tests pass locally.

 3054 passed, 2383 warnings in 46.96s

### Checklist

- [X] I have read the [CONTRIBUTING.md](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) document.
- [X] I have performed a self-review of my own code.
- [X] I have commented my code, particularly in hard-to-understand areas.
- [X] I have added tests that prove my fix is effective or that my feature works.
- [X] New and existing unit tests pass locally with my changes.
- [X] I have manually tested my changes end-to-end.
- [X] Any dependent changes have been merged and published in downstream modules.

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/3430 from dylan-apex:more-detailed-list-apps e6864fd61a673da5fd2fb28d2d7d72cb90f5af0a
PiperOrigin-RevId: 834907771
This commit is contained in:
Dylan
2025-11-20 14:15:06 -08:00
committed by Copybara-Service
parent cd54f48fed
commit b57fe5f459
4 changed files with 106 additions and 1 deletions
+19 -1
View File
@@ -280,6 +280,17 @@ class ListMetricsInfoResponse(common.BaseModel):
metrics_info: list[MetricInfo] metrics_info: list[MetricInfo]
class AppInfo(common.BaseModel):
name: str
root_agent_name: str
description: str
language: Literal["yaml", "python"]
class ListAppsResponse(common.BaseModel):
apps: list[AppInfo]
def _setup_telemetry( def _setup_telemetry(
otel_to_cloud: bool = False, otel_to_cloud: bool = False,
internal_exporters: Optional[list[SpanProcessor]] = None, internal_exporters: Optional[list[SpanProcessor]] = None,
@@ -699,7 +710,14 @@ class AdkWebServer:
) )
@app.get("/list-apps") @app.get("/list-apps")
async def list_apps() -> list[str]: async def list_apps(
detailed: bool = Query(
default=False, description="Return detailed app information"
)
) -> list[str] | ListAppsResponse:
if detailed:
apps_info = self.agent_loader.list_agents_detailed()
return ListAppsResponse(apps=[AppInfo(**app) for app in apps_info])
return self.agent_loader.list_agents() return self.agent_loader.list_agents()
@app.get("/debug/trace/{event_id}", tags=[TAG_DEBUG]) @app.get("/debug/trace/{event_id}", tags=[TAG_DEBUG])
+46
View File
@@ -20,6 +20,8 @@ import logging
import os import os
from pathlib import Path from pathlib import Path
import sys import sys
from typing import Any
from typing import Literal
from typing import Optional from typing import Optional
from typing import Union from typing import Union
@@ -341,6 +343,50 @@ class AgentLoader(BaseAgentLoader):
agent_names.sort() agent_names.sort()
return agent_names return agent_names
def list_agents_detailed(self) -> list[dict[str, Any]]:
"""Lists all agents with detailed metadata (name, description, type)."""
agent_names = self.list_agents()
apps_info = []
for agent_name in agent_names:
try:
loaded = self.load_agent(agent_name)
if isinstance(loaded, App):
agent = loaded.root_agent
else:
agent = loaded
language = self._determine_agent_language(agent_name)
app_info = {
"name": agent_name,
"root_agent_name": agent.name,
"description": agent.description,
"language": language,
}
apps_info.append(app_info)
except Exception as e:
logger.error("Failed to load agent '%s': %s", agent_name, e)
continue
return apps_info
def _determine_agent_language(
self, agent_name: str
) -> Literal["yaml", "python"]:
"""Determine the type of agent based on file structure."""
base_path = Path.cwd() / self.agents_dir / agent_name
if (base_path / "root_agent.yaml").exists():
return "yaml"
elif (base_path / "agent.py").exists():
return "python"
elif (base_path / "__init__.py").exists():
return "python"
raise ValueError(f"Could not determine agent type for '{agent_name}'.")
def remove_agent_from_cache(self, agent_name: str): def remove_agent_from_cache(self, agent_name: str):
# Clear module cache for the agent and its submodules # Clear module cache for the agent and its submodules
keys_to_delete = [ keys_to_delete = [
@@ -18,6 +18,7 @@ from __future__ import annotations
from abc import ABC from abc import ABC
from abc import abstractmethod from abc import abstractmethod
from typing import Any
from typing import Union from typing import Union
from ...agents.base_agent import BaseAgent from ...agents.base_agent import BaseAgent
@@ -34,3 +35,15 @@ class BaseAgentLoader(ABC):
@abstractmethod @abstractmethod
def list_agents(self) -> list[str]: def list_agents(self) -> list[str]:
"""Lists all agents available in the agent loader in alphabetical order.""" """Lists all agents available in the agent loader in alphabetical order."""
def list_agents_detailed(self) -> list[dict[str, Any]]:
agent_names = self.list_agents()
return [
{
'name': name,
'display_name': None,
'description': None,
'type': None,
}
for name in agent_names
]
+28
View File
@@ -190,6 +190,14 @@ def mock_agent_loader():
def list_agents(self): def list_agents(self):
return ["test_app"] return ["test_app"]
def list_agents_detailed(self):
return [{
"name": "test_app",
"root_agent_name": "test_agent",
"description": "A test agent for unit testing",
"language": "python",
}]
return MockAgentLoader(".") return MockAgentLoader(".")
@@ -548,6 +556,26 @@ def test_list_apps(test_app):
logger.info(f"Listed apps: {data}") logger.info(f"Listed apps: {data}")
def test_list_apps_detailed(test_app):
"""Test listing available applications with detailed metadata."""
response = test_app.get("/list-apps?detailed=true")
assert response.status_code == 200
data = response.json()
assert isinstance(data, dict)
assert "apps" in data
assert isinstance(data["apps"], list)
for app in data["apps"]:
assert "name" in app
assert "rootAgentName" in app
assert "description" in app
assert "language" in app
assert app["language"] in ["yaml", "python"]
logger.info(f"Listed apps: {data}")
def test_create_session_with_id(test_app, test_session_info): def test_create_session_with_id(test_app, test_session_info):
"""Test creating a session with a specific ID.""" """Test creating a session with a specific ID."""
new_session_id = "new_session_id" new_session_id = "new_session_id"