Skip to content

Commit b57fe5f

Browse files
dylan-apexcopybara-github
authored andcommitted
feat(web): add list-apps-detailed endpoint
Merge #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=#3430 from dylan-apex:more-detailed-list-apps e6864fd PiperOrigin-RevId: 834907771
1 parent cd54f48 commit b57fe5f

File tree

4 files changed

+106
-1
lines changed

4 files changed

+106
-1
lines changed

src/google/adk/cli/adk_web_server.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,17 @@ class ListMetricsInfoResponse(common.BaseModel):
280280
metrics_info: list[MetricInfo]
281281

282282

283+
class AppInfo(common.BaseModel):
284+
name: str
285+
root_agent_name: str
286+
description: str
287+
language: Literal["yaml", "python"]
288+
289+
290+
class ListAppsResponse(common.BaseModel):
291+
apps: list[AppInfo]
292+
293+
283294
def _setup_telemetry(
284295
otel_to_cloud: bool = False,
285296
internal_exporters: Optional[list[SpanProcessor]] = None,
@@ -699,7 +710,14 @@ async def internal_lifespan(app: FastAPI):
699710
)
700711

701712
@app.get("/list-apps")
702-
async def list_apps() -> list[str]:
713+
async def list_apps(
714+
detailed: bool = Query(
715+
default=False, description="Return detailed app information"
716+
)
717+
) -> list[str] | ListAppsResponse:
718+
if detailed:
719+
apps_info = self.agent_loader.list_agents_detailed()
720+
return ListAppsResponse(apps=[AppInfo(**app) for app in apps_info])
703721
return self.agent_loader.list_agents()
704722

705723
@app.get("/debug/trace/{event_id}", tags=[TAG_DEBUG])

src/google/adk/cli/utils/agent_loader.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import os
2121
from pathlib import Path
2222
import sys
23+
from typing import Any
24+
from typing import Literal
2325
from typing import Optional
2426
from typing import Union
2527

@@ -341,6 +343,50 @@ def list_agents(self) -> list[str]:
341343
agent_names.sort()
342344
return agent_names
343345

346+
def list_agents_detailed(self) -> list[dict[str, Any]]:
347+
"""Lists all agents with detailed metadata (name, description, type)."""
348+
agent_names = self.list_agents()
349+
apps_info = []
350+
351+
for agent_name in agent_names:
352+
try:
353+
loaded = self.load_agent(agent_name)
354+
if isinstance(loaded, App):
355+
agent = loaded.root_agent
356+
else:
357+
agent = loaded
358+
359+
language = self._determine_agent_language(agent_name)
360+
361+
app_info = {
362+
"name": agent_name,
363+
"root_agent_name": agent.name,
364+
"description": agent.description,
365+
"language": language,
366+
}
367+
apps_info.append(app_info)
368+
369+
except Exception as e:
370+
logger.error("Failed to load agent '%s': %s", agent_name, e)
371+
continue
372+
373+
return apps_info
374+
375+
def _determine_agent_language(
376+
self, agent_name: str
377+
) -> Literal["yaml", "python"]:
378+
"""Determine the type of agent based on file structure."""
379+
base_path = Path.cwd() / self.agents_dir / agent_name
380+
381+
if (base_path / "root_agent.yaml").exists():
382+
return "yaml"
383+
elif (base_path / "agent.py").exists():
384+
return "python"
385+
elif (base_path / "__init__.py").exists():
386+
return "python"
387+
388+
raise ValueError(f"Could not determine agent type for '{agent_name}'.")
389+
344390
def remove_agent_from_cache(self, agent_name: str):
345391
# Clear module cache for the agent and its submodules
346392
keys_to_delete = [

src/google/adk/cli/utils/base_agent_loader.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from abc import ABC
2020
from abc import abstractmethod
21+
from typing import Any
2122
from typing import Union
2223

2324
from ...agents.base_agent import BaseAgent
@@ -34,3 +35,15 @@ def load_agent(self, agent_name: str) -> Union[BaseAgent, App]:
3435
@abstractmethod
3536
def list_agents(self) -> list[str]:
3637
"""Lists all agents available in the agent loader in alphabetical order."""
38+
39+
def list_agents_detailed(self) -> list[dict[str, Any]]:
40+
agent_names = self.list_agents()
41+
return [
42+
{
43+
'name': name,
44+
'display_name': None,
45+
'description': None,
46+
'type': None,
47+
}
48+
for name in agent_names
49+
]

tests/unittests/cli/test_fast_api.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,14 @@ def load_agent(self, app_name):
190190
def list_agents(self):
191191
return ["test_app"]
192192

193+
def list_agents_detailed(self):
194+
return [{
195+
"name": "test_app",
196+
"root_agent_name": "test_agent",
197+
"description": "A test agent for unit testing",
198+
"language": "python",
199+
}]
200+
193201
return MockAgentLoader(".")
194202

195203

@@ -548,6 +556,26 @@ def test_list_apps(test_app):
548556
logger.info(f"Listed apps: {data}")
549557

550558

559+
def test_list_apps_detailed(test_app):
560+
"""Test listing available applications with detailed metadata."""
561+
response = test_app.get("/list-apps?detailed=true")
562+
563+
assert response.status_code == 200
564+
data = response.json()
565+
assert isinstance(data, dict)
566+
assert "apps" in data
567+
assert isinstance(data["apps"], list)
568+
569+
for app in data["apps"]:
570+
assert "name" in app
571+
assert "rootAgentName" in app
572+
assert "description" in app
573+
assert "language" in app
574+
assert app["language"] in ["yaml", "python"]
575+
576+
logger.info(f"Listed apps: {data}")
577+
578+
551579
def test_create_session_with_id(test_app, test_session_info):
552580
"""Test creating a session with a specific ID."""
553581
new_session_id = "new_session_id"

0 commit comments

Comments
 (0)