Skip to content

Commit 1b7d1e3

Browse files
feat: add ping support for a2a
feat: add ping support for a2a
2 parents 31bddaf + c35dea8 commit 1b7d1e3

File tree

4 files changed

+98
-30
lines changed

4 files changed

+98
-30
lines changed

agentkit/apps/a2a_app/a2a_app.py

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414

1515
import logging
1616
import os
17-
from typing import Callable, override
18-
1917
import uvicorn
18+
import inspect
19+
20+
from typing import Callable, override
2021
from a2a.server.agent_execution import AgentExecutor
2122
from a2a.server.agent_execution.context import RequestContext
2223
from a2a.server.apps import A2AStarletteApplication
@@ -26,8 +27,9 @@
2627
from a2a.server.tasks.task_store import TaskStore
2728
from a2a.types import AgentCard
2829
from starlette.applications import Starlette
29-
from starlette.responses import JSONResponse
30+
from starlette.responses import JSONResponse, Response
3031
from starlette.routing import Route
32+
from starlette.requests import Request
3133

3234
from agentkit.apps.a2a_app.telemetry import telemetry
3335
from agentkit.apps.base_app import BaseAgentkitApp
@@ -41,9 +43,7 @@ async def wrapper(*args, **kwargs):
4143
context: RequestContext = args[1]
4244
event_queue: EventQueue = args[2]
4345

44-
with telemetry.tracer.start_as_current_span(
45-
name="a2a_invocation"
46-
) as span:
46+
with telemetry.tracer.start_as_current_span(name="a2a_invocation") as span:
4747
exception = None
4848
try:
4949
result = await execute_func(
@@ -75,6 +75,7 @@ def __init__(self) -> None:
7575

7676
self._agent_executor: AgentExecutor | None = None
7777
self._task_store: TaskStore | None = None
78+
self._ping_func: Callable | None = None
7879

7980
def agent_executor(self, **kwargs) -> Callable:
8081
"""Wrap an AgentExecutor class, init it, then bind it to the app instance."""
@@ -86,9 +87,7 @@ def wrapper(cls: type) -> type[AgentExecutor]:
8687
)
8788

8889
if self._agent_executor:
89-
raise RuntimeError(
90-
"An executor is already bound to this app instance."
91-
)
90+
raise RuntimeError("An executor is already bound to this app instance.")
9291

9392
# Wrap the execute method for intercepting context and event_queue
9493
cls.execute = _wrap_agent_executor_execute_func(cls.execute)
@@ -119,6 +118,50 @@ def wrapper(cls: type) -> type[TaskStore]:
119118

120119
return wrapper
121120

121+
def ping(self, func: Callable) -> Callable:
122+
"""Register a zero-argument health check function and expose it via GET /ping.
123+
124+
The function must accept no arguments and should return either a string or a dict.
125+
The response shape mirrors SimpleApp: {"status": <str|dict>}.
126+
"""
127+
# Ensure zero-argument function similar to SimpleApp
128+
if len(list(inspect.signature(func).parameters.keys())) != 0:
129+
raise AssertionError(
130+
f"Health check function `{func.__name__}` should not receive any arguments."
131+
)
132+
133+
self._ping_func = func
134+
return func
135+
136+
def _format_ping_status(self, result: str | dict) -> dict:
137+
# Align behavior with SimpleApp: always wrap into {"status": result}
138+
if isinstance(result, (str, dict)):
139+
return {"status": result}
140+
logger.error(
141+
f"Health check function {getattr(self._ping_func, '__name__', 'unknown')} must return `dict` or `str` type."
142+
)
143+
return {"status": "error", "message": "Invalid response type."}
144+
145+
async def ping_endpoint(self, request: Request) -> Response:
146+
if not self._ping_func:
147+
logger.error("Ping handler function is not set")
148+
return Response(status_code=404)
149+
150+
try:
151+
result = (
152+
await self._ping_func()
153+
if inspect.iscoroutinefunction(self._ping_func)
154+
else self._ping_func()
155+
)
156+
payload = self._format_ping_status(result)
157+
return JSONResponse(content=payload)
158+
except Exception as e:
159+
logger.exception("Ping handler function failed: %s", e)
160+
return JSONResponse(
161+
content={"status": "error", "message": str(e)},
162+
status_code=500,
163+
)
164+
122165
def add_env_detect_route(self, app: Starlette):
123166
def is_agentkit_runtime() -> bool:
124167
if os.getenv("RUNTIME_IAM_ROLE_TRN", ""):
@@ -136,6 +179,9 @@ def is_agentkit_runtime() -> bool:
136179
)
137180
app.routes.append(route)
138181

182+
def add_ping_route(self, app: Starlette):
183+
app.add_route("/ping", self.ping_endpoint, methods=["GET"])
184+
139185
@override
140186
def run(self, agent_card: AgentCard, host: str, port: int = 8000):
141187
if not self._agent_executor:
@@ -155,6 +201,8 @@ def run(self, agent_card: AgentCard, host: str, port: int = 8000):
155201
),
156202
).build()
157203

204+
# Register routes in the same style
205+
self.add_ping_route(a2a_app)
158206
self.add_env_detect_route(a2a_app)
159207

160208
uvicorn.run(a2a_app, host=host, port=port)

agentkit/apps/mcp_app/mcp_app.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ def tool(self, func: Callable) -> Callable:
4040
@wraps(func)
4141
async def async_wrapper(*args, **kwargs) -> Any:
4242
# with tracer.start_as_current_span("tool") as span:
43-
with telemetry.tracer.start_as_current_span(
44-
name="tool"
45-
) as span:
43+
with telemetry.tracer.start_as_current_span(name="tool") as span:
4644
exception = None
4745
try:
4846
result = await func(*args, **kwargs)
@@ -70,9 +68,7 @@ async def async_wrapper(*args, **kwargs) -> Any:
7068
@wraps(func)
7169
def sync_wrapper(*args, **kwargs) -> Any:
7270
# with tracer.start_as_current_span("tool") as span:
73-
with telemetry.tracer.start_as_current_span(
74-
name="tool"
75-
) as span:
71+
with telemetry.tracer.start_as_current_span(name="tool") as span:
7672
exception = None
7773
try:
7874
result = func(*args, **kwargs)
@@ -100,9 +96,7 @@ def agent_as_a_tool(self, func: Callable) -> Callable:
10096

10197
@wraps(func)
10298
async def async_wrapper(*args, **kwargs) -> Any:
103-
with telemetry.tracer.start_as_current_span(
104-
name="tool"
105-
) as span:
99+
with telemetry.tracer.start_as_current_span(name="tool") as span:
106100
exception = None
107101
try:
108102
result = await func(*args, **kwargs)
@@ -126,9 +120,7 @@ async def async_wrapper(*args, **kwargs) -> Any:
126120

127121
@wraps(func)
128122
def sync_wrapper(*args, **kwargs) -> Any:
129-
with telemetry.tracer.start_as_current_span(
130-
name="tool"
131-
) as span:
123+
with telemetry.tracer.start_as_current_span(name="tool") as span:
132124
exception = None
133125
try:
134126
result = func(*args, **kwargs)

agentkit/toolkit/resources/samples/a2a.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,19 @@
6363
class MyAgentExecutor(A2aAgentExecutor):
6464
pass
6565

66+
@a2a_app.ping
67+
def ping() -> str:
68+
return "pong!"
6669

6770
if __name__ == "__main__":
6871
from a2a.types import AgentCard, AgentProvider, AgentSkill, AgentCapabilities
6972

7073
agent_card = AgentCard(
71-
capabilities=AgentCapabilities(streaming=True), # 启用流式
74+
capabilities=AgentCapabilities(streaming=True),
7275
description=agent.description,
7376
name=agent.name,
74-
defaultInputModes=["text"],
75-
defaultOutputModes=["text"],
77+
default_input_modes=["text"],
78+
default_output_modes=["text"],
7679
provider=AgentProvider(organization="veadk", url=""),
7780
skills=[AgentSkill(id="0", name="chat", description="Chat", tags=["chat"])],
7881
url="http://0.0.0.0:8000",

agentkit/toolkit/runners/ve_agentkit.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,28 @@ def status(self, config: VeAgentkitRunnerConfig) -> StatusResult:
248248
},
249249
timeout=10,
250250
)
251-
ping_status = ping_response.status_code == 200
251+
if ping_response.status_code == 200:
252+
ping_status = True
253+
elif ping_response.status_code in (404, 405):
254+
# Fallback: try /health for SimpleApp compatibility
255+
try:
256+
health_response = requests.get(
257+
urljoin(public_endpoint, "health"),
258+
headers={
259+
"Authorization": f"Bearer {runner_config.runtime_apikey}"
260+
},
261+
timeout=10,
262+
)
263+
if health_response.status_code == 200:
264+
ping_status = True
265+
else:
266+
ping_status = None # Endpoint reachable but health route not available
267+
except Exception:
268+
# Endpoint reachable (ping returned 404/405), but health check failed
269+
ping_status = None
270+
else:
271+
# Non-200 status indicates server responded but not healthy
272+
ping_status = False
252273
except Exception as e:
253274
logger.error(f"Failed to check endpoint connectivity: {str(e)}")
254275
ping_status = False
@@ -265,11 +286,15 @@ def status(self, config: VeAgentkitRunnerConfig) -> StatusResult:
265286
status=status,
266287
endpoint_url=public_endpoint,
267288
service_id=runner_config.runtime_id,
268-
health="healthy"
269-
if ping_status
270-
else "unhealthy"
271-
if ping_status is False
272-
else None,
289+
health=(
290+
"healthy"
291+
if ping_status is True
292+
else "unhealthy"
293+
if ping_status is False
294+
else "unknown"
295+
if ping_status is None
296+
else None
297+
),
273298
metadata={
274299
"runtime_id": runner_config.runtime_id,
275300
"runtime_name": runtime.name

0 commit comments

Comments
 (0)