Skip to content

Commit e77a4f5

Browse files
committed
feat: Add wait_until_ready() method for agent deployment polling
Resolves #40 - Add wait_until_ready() method to AgentsResource and AsyncAgentsResource - Add AgentDeploymentError and AgentDeploymentTimeoutError exceptions - Add comprehensive test coverage for sync and async variants - Add example usage documentation Users can now call client.agents.wait_until_ready(agent_id) instead of writing custom polling loops to wait for agent deployment completion. The method polls the agent status every 5 seconds (configurable) until: - SUCCESS: Agent reaches STATUS_RUNNING (returns AgentRetrieveResponse) - FAILURE: Agent reaches STATUS_FAILED/STATUS_UNDEPLOYMENT_FAILED/STATUS_DELETED (raises AgentDeploymentError) - TIMEOUT: Agent doesn't reach STATUS_RUNNING within timeout period (raises AgentDeploymentTimeoutError) Default timeout is 300 seconds (5 minutes) and poll_interval is 5 seconds. Both sync and async implementations are provided.
1 parent 0dccc0c commit e77a4f5

File tree

4 files changed

+497
-0
lines changed

4 files changed

+497
-0
lines changed

examples/agent_wait_until_ready.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""
2+
Example: Wait for Agent Deployment to Complete
3+
4+
This example demonstrates how to use the wait_until_ready() method to wait for
5+
an agent to finish deploying before using it.
6+
"""
7+
8+
from gradient import Gradient
9+
from gradient._exceptions import AgentDeploymentError, AgentDeploymentTimeoutError
10+
11+
# Initialize the Gradient client
12+
client = Gradient()
13+
14+
# Create a new agent
15+
agent_response = client.agents.create(
16+
name="My Agent",
17+
instruction="You are a helpful assistant",
18+
model_uuid="<your-model-uuid>",
19+
region="nyc1",
20+
)
21+
22+
agent_id = agent_response.agent.uuid if agent_response.agent else None
23+
24+
if agent_id:
25+
print(f"Agent created with ID: {agent_id}")
26+
print("Waiting for agent to be ready...")
27+
28+
try:
29+
# Wait for the agent to be deployed and ready
30+
# This will poll the agent status every 5 seconds (default)
31+
# and wait up to 5 minutes (default timeout=300 seconds)
32+
ready_agent = client.agents.wait_until_ready(
33+
agent_id,
34+
poll_interval=5.0, # Check every 5 seconds
35+
timeout=300.0, # Wait up to 5 minutes
36+
)
37+
38+
if ready_agent.agent and ready_agent.agent.deployment:
39+
print(f"Agent is ready! Status: {ready_agent.agent.deployment.status}")
40+
print(f"Agent URL: {ready_agent.agent.url}")
41+
42+
# Now you can use the agent
43+
# ...
44+
45+
except AgentDeploymentError as e:
46+
print(f"Agent deployment failed: {e}")
47+
print(f"Failed status: {e.status}")
48+
49+
except AgentDeploymentTimeoutError as e:
50+
print(f"Agent deployment timed out: {e}")
51+
print(f"Agent ID: {e.agent_id}")
52+
53+
except Exception as e:
54+
print(f"Unexpected error: {e}")
55+
56+
57+
# Async example
58+
from gradient import AsyncGradient
59+
60+
61+
async def main():
62+
async_client = AsyncGradient()
63+
64+
# Create a new agent
65+
agent_response = await async_client.agents.create(
66+
name="My Async Agent",
67+
instruction="You are a helpful assistant",
68+
model_uuid="<your-model-uuid>",
69+
region="nyc1",
70+
)
71+
72+
agent_id = agent_response.agent.uuid if agent_response.agent else None
73+
74+
if agent_id:
75+
print(f"Agent created with ID: {agent_id}")
76+
print("Waiting for agent to be ready...")
77+
78+
try:
79+
# Wait for the agent to be deployed and ready (async)
80+
ready_agent = await async_client.agents.wait_until_ready(
81+
agent_id,
82+
poll_interval=5.0,
83+
timeout=300.0,
84+
)
85+
86+
if ready_agent.agent and ready_agent.agent.deployment:
87+
print(f"Agent is ready! Status: {ready_agent.agent.deployment.status}")
88+
print(f"Agent URL: {ready_agent.agent.url}")
89+
90+
except AgentDeploymentError as e:
91+
print(f"Agent deployment failed: {e}")
92+
print(f"Failed status: {e.status}")
93+
94+
except AgentDeploymentTimeoutError as e:
95+
print(f"Agent deployment timed out: {e}")
96+
print(f"Agent ID: {e.agent_id}")
97+
98+
99+
# Uncomment to run async example
100+
# asyncio.run(main())

src/gradient/_exceptions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"UnprocessableEntityError",
1616
"RateLimitError",
1717
"InternalServerError",
18+
"AgentDeploymentError",
19+
"AgentDeploymentTimeoutError",
1820
]
1921

2022

@@ -106,3 +108,19 @@ class RateLimitError(APIStatusError):
106108

107109
class InternalServerError(APIStatusError):
108110
pass
111+
112+
113+
class AgentDeploymentError(GradientError):
114+
"""Raised when an agent deployment fails."""
115+
116+
def __init__(self, message: str, status: str) -> None:
117+
super().__init__(message)
118+
self.status = status
119+
120+
121+
class AgentDeploymentTimeoutError(GradientError):
122+
"""Raised when waiting for an agent deployment times out."""
123+
124+
def __init__(self, message: str, agent_id: str) -> None:
125+
super().__init__(message)
126+
self.agent_id = agent_id

src/gradient/resources/agents/agents.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import time
56
import httpx
67

78
from .routes import (
@@ -612,6 +613,93 @@ def update_status(
612613
cast_to=AgentUpdateStatusResponse,
613614
)
614615

616+
def wait_until_ready(
617+
self,
618+
uuid: str,
619+
*,
620+
timeout: float = 300.0,
621+
poll_interval: float = 5.0,
622+
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
623+
# The extra values given here take precedence over values defined on the client or passed to this method.
624+
extra_headers: Headers | None = None,
625+
extra_query: Query | None = None,
626+
extra_body: Body | None = None,
627+
) -> AgentRetrieveResponse:
628+
"""Wait for an agent to be ready (deployment status is STATUS_RUNNING).
629+
630+
This method polls the agent status until it reaches STATUS_RUNNING or a terminal
631+
error state. It handles timeout and deployment failures automatically.
632+
633+
Args:
634+
uuid: The unique identifier of the agent to wait for
635+
636+
timeout: Maximum time to wait in seconds (default: 300 seconds / 5 minutes)
637+
638+
poll_interval: Time to wait between status checks in seconds (default: 5 seconds)
639+
640+
extra_headers: Send extra headers
641+
642+
extra_query: Add additional query parameters to the request
643+
644+
extra_body: Add additional JSON properties to the request
645+
646+
Returns:
647+
AgentRetrieveResponse: The agent response when it reaches STATUS_RUNNING
648+
649+
Raises:
650+
AgentDeploymentError: If the agent deployment fails (STATUS_FAILED,
651+
STATUS_UNDEPLOYMENT_FAILED, or STATUS_DELETED)
652+
AgentDeploymentTimeoutError: If the agent doesn't reach STATUS_RUNNING
653+
within the timeout period
654+
ValueError: If uuid is empty
655+
"""
656+
from ..._exceptions import AgentDeploymentError, AgentDeploymentTimeoutError
657+
658+
if not uuid:
659+
raise ValueError(f"Expected a non-empty value for `uuid` but received {uuid!r}")
660+
661+
start_time = time.time()
662+
663+
while True:
664+
agent_response = self.retrieve(
665+
uuid,
666+
extra_headers=extra_headers,
667+
extra_query=extra_query,
668+
extra_body=extra_body,
669+
)
670+
671+
# Check if agent and deployment exist
672+
if agent_response.agent and agent_response.agent.deployment:
673+
status = agent_response.agent.deployment.status
674+
675+
# Success case
676+
if status == "STATUS_RUNNING":
677+
return agent_response
678+
679+
# Failure cases
680+
if status in ("STATUS_FAILED", "STATUS_UNDEPLOYMENT_FAILED", "STATUS_DELETED"):
681+
raise AgentDeploymentError(
682+
f"Agent deployment failed with status: {status}",
683+
status=status or "UNKNOWN",
684+
)
685+
686+
# Check timeout
687+
elapsed_time = time.time() - start_time
688+
if elapsed_time >= timeout:
689+
current_status = (
690+
agent_response.agent.deployment.status
691+
if agent_response.agent and agent_response.agent.deployment
692+
else "UNKNOWN"
693+
)
694+
raise AgentDeploymentTimeoutError(
695+
f"Agent did not reach STATUS_RUNNING within {timeout} seconds. "
696+
f"Current status: {current_status}",
697+
agent_id=uuid,
698+
)
699+
700+
# Wait before polling again
701+
time.sleep(poll_interval)
702+
615703

616704
class AsyncAgentsResource(AsyncAPIResource):
617705
@cached_property
@@ -1108,6 +1196,94 @@ async def update_status(
11081196
cast_to=AgentUpdateStatusResponse,
11091197
)
11101198

1199+
async def wait_until_ready(
1200+
self,
1201+
uuid: str,
1202+
*,
1203+
timeout: float = 300.0,
1204+
poll_interval: float = 5.0,
1205+
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
1206+
# The extra values given here take precedence over values defined on the client or passed to this method.
1207+
extra_headers: Headers | None = None,
1208+
extra_query: Query | None = None,
1209+
extra_body: Body | None = None,
1210+
) -> AgentRetrieveResponse:
1211+
"""Wait for an agent to be ready (deployment status is STATUS_RUNNING).
1212+
1213+
This method polls the agent status until it reaches STATUS_RUNNING or a terminal
1214+
error state. It handles timeout and deployment failures automatically.
1215+
1216+
Args:
1217+
uuid: The unique identifier of the agent to wait for
1218+
1219+
timeout: Maximum time to wait in seconds (default: 300 seconds / 5 minutes)
1220+
1221+
poll_interval: Time to wait between status checks in seconds (default: 5 seconds)
1222+
1223+
extra_headers: Send extra headers
1224+
1225+
extra_query: Add additional query parameters to the request
1226+
1227+
extra_body: Add additional JSON properties to the request
1228+
1229+
Returns:
1230+
AgentRetrieveResponse: The agent response when it reaches STATUS_RUNNING
1231+
1232+
Raises:
1233+
AgentDeploymentError: If the agent deployment fails (STATUS_FAILED,
1234+
STATUS_UNDEPLOYMENT_FAILED, or STATUS_DELETED)
1235+
AgentDeploymentTimeoutError: If the agent doesn't reach STATUS_RUNNING
1236+
within the timeout period
1237+
ValueError: If uuid is empty
1238+
"""
1239+
import asyncio
1240+
from ..._exceptions import AgentDeploymentError, AgentDeploymentTimeoutError
1241+
1242+
if not uuid:
1243+
raise ValueError(f"Expected a non-empty value for `uuid` but received {uuid!r}")
1244+
1245+
start_time = time.time()
1246+
1247+
while True:
1248+
agent_response = await self.retrieve(
1249+
uuid,
1250+
extra_headers=extra_headers,
1251+
extra_query=extra_query,
1252+
extra_body=extra_body,
1253+
)
1254+
1255+
# Check if agent and deployment exist
1256+
if agent_response.agent and agent_response.agent.deployment:
1257+
status = agent_response.agent.deployment.status
1258+
1259+
# Success case
1260+
if status == "STATUS_RUNNING":
1261+
return agent_response
1262+
1263+
# Failure cases
1264+
if status in ("STATUS_FAILED", "STATUS_UNDEPLOYMENT_FAILED", "STATUS_DELETED"):
1265+
raise AgentDeploymentError(
1266+
f"Agent deployment failed with status: {status}",
1267+
status=status or "UNKNOWN",
1268+
)
1269+
1270+
# Check timeout
1271+
elapsed_time = time.time() - start_time
1272+
if elapsed_time >= timeout:
1273+
current_status = (
1274+
agent_response.agent.deployment.status
1275+
if agent_response.agent and agent_response.agent.deployment
1276+
else "UNKNOWN"
1277+
)
1278+
raise AgentDeploymentTimeoutError(
1279+
f"Agent did not reach STATUS_RUNNING within {timeout} seconds. "
1280+
f"Current status: {current_status}",
1281+
agent_id=uuid,
1282+
)
1283+
1284+
# Wait before polling again
1285+
await asyncio.sleep(poll_interval)
1286+
11111287

11121288
class AgentsResourceWithRawResponse:
11131289
def __init__(self, agents: AgentsResource) -> None:
@@ -1134,6 +1310,9 @@ def __init__(self, agents: AgentsResource) -> None:
11341310
self.update_status = to_raw_response_wrapper(
11351311
agents.update_status,
11361312
)
1313+
self.wait_until_ready = to_raw_response_wrapper(
1314+
agents.wait_until_ready,
1315+
)
11371316

11381317
@cached_property
11391318
def api_keys(self) -> APIKeysResourceWithRawResponse:
@@ -1201,6 +1380,9 @@ def __init__(self, agents: AsyncAgentsResource) -> None:
12011380
self.update_status = async_to_raw_response_wrapper(
12021381
agents.update_status,
12031382
)
1383+
self.wait_until_ready = async_to_raw_response_wrapper(
1384+
agents.wait_until_ready,
1385+
)
12041386

12051387
@cached_property
12061388
def api_keys(self) -> AsyncAPIKeysResourceWithRawResponse:
@@ -1268,6 +1450,9 @@ def __init__(self, agents: AgentsResource) -> None:
12681450
self.update_status = to_streamed_response_wrapper(
12691451
agents.update_status,
12701452
)
1453+
self.wait_until_ready = to_streamed_response_wrapper(
1454+
agents.wait_until_ready,
1455+
)
12711456

12721457
@cached_property
12731458
def api_keys(self) -> APIKeysResourceWithStreamingResponse:
@@ -1335,6 +1520,9 @@ def __init__(self, agents: AsyncAgentsResource) -> None:
13351520
self.update_status = async_to_streamed_response_wrapper(
13361521
agents.update_status,
13371522
)
1523+
self.wait_until_ready = async_to_streamed_response_wrapper(
1524+
agents.wait_until_ready,
1525+
)
13381526

13391527
@cached_property
13401528
def api_keys(self) -> AsyncAPIKeysResourceWithStreamingResponse:

0 commit comments

Comments
 (0)