mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
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:
@@ -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])
|
||||||
|
|||||||
@@ -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
|
||||||
|
]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user