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]
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(
otel_to_cloud: bool = False,
internal_exporters: Optional[list[SpanProcessor]] = None,
@@ -699,7 +710,14 @@ class AdkWebServer:
)
@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()
@app.get("/debug/trace/{event_id}", tags=[TAG_DEBUG])
+46
View File
@@ -20,6 +20,8 @@ import logging
import os
from pathlib import Path
import sys
from typing import Any
from typing import Literal
from typing import Optional
from typing import Union
@@ -341,6 +343,50 @@ class AgentLoader(BaseAgentLoader):
agent_names.sort()
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):
# Clear module cache for the agent and its submodules
keys_to_delete = [
@@ -18,6 +18,7 @@ from __future__ import annotations
from abc import ABC
from abc import abstractmethod
from typing import Any
from typing import Union
from ...agents.base_agent import BaseAgent
@@ -34,3 +35,15 @@ class BaseAgentLoader(ABC):
@abstractmethod
def list_agents(self) -> list[str]:
"""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):
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(".")
@@ -548,6 +556,26 @@ def test_list_apps(test_app):
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):
"""Test creating a session with a specific ID."""
new_session_id = "new_session_id"