Skip to content

Commit dcef3d5

Browse files
cipheraxatbbatha
andauthored
feat: Add wait_until_ready() method for agent deployment polling (#56)
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. -------------------- Co-authored-by: Ben Batha <[email protected]>
1 parent 7a0515a commit dcef3d5

File tree

4 files changed

+497
-1
lines changed

4 files changed

+497
-1
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() -> None:
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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2-
32
from __future__ import annotations
43

4+
import time
5+
56
import httpx
67

78
from .routes import (
@@ -612,6 +613,92 @@ 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,
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. Current status: {current_status}",
696+
agent_id=uuid,
697+
)
698+
699+
# Wait before polling again
700+
time.sleep(poll_interval)
701+
615702

616703
class AsyncAgentsResource(AsyncAPIResource):
617704
@cached_property
@@ -1108,6 +1195,94 @@ async def update_status(
11081195
cast_to=AgentUpdateStatusResponse,
11091196
)
11101197

1198+
async def wait_until_ready(
1199+
self,
1200+
uuid: str,
1201+
*,
1202+
timeout: float = 300.0,
1203+
poll_interval: float = 5.0,
1204+
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
1205+
# The extra values given here take precedence over values defined on the client or passed to this method.
1206+
extra_headers: Headers | None = None,
1207+
extra_query: Query | None = None,
1208+
extra_body: Body | None = None,
1209+
) -> AgentRetrieveResponse:
1210+
"""Wait for an agent to be ready (deployment status is STATUS_RUNNING).
1211+
1212+
This method polls the agent status until it reaches STATUS_RUNNING or a terminal
1213+
error state. It handles timeout and deployment failures automatically.
1214+
1215+
Args:
1216+
uuid: The unique identifier of the agent to wait for
1217+
1218+
timeout: Maximum time to wait in seconds (default: 300 seconds / 5 minutes)
1219+
1220+
poll_interval: Time to wait between status checks in seconds (default: 5 seconds)
1221+
1222+
extra_headers: Send extra headers
1223+
1224+
extra_query: Add additional query parameters to the request
1225+
1226+
extra_body: Add additional JSON properties to the request
1227+
1228+
Returns:
1229+
AgentRetrieveResponse: The agent response when it reaches STATUS_RUNNING
1230+
1231+
Raises:
1232+
AgentDeploymentError: If the agent deployment fails (STATUS_FAILED,
1233+
STATUS_UNDEPLOYMENT_FAILED, or STATUS_DELETED)
1234+
AgentDeploymentTimeoutError: If the agent doesn't reach STATUS_RUNNING
1235+
within the timeout period
1236+
ValueError: If uuid is empty
1237+
"""
1238+
import asyncio
1239+
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,
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. Current status: {current_status}",
1280+
agent_id=uuid,
1281+
)
1282+
1283+
# Wait before polling again
1284+
await asyncio.sleep(poll_interval)
1285+
11111286

11121287
class AgentsResourceWithRawResponse:
11131288
def __init__(self, agents: AgentsResource) -> None:
@@ -1134,6 +1309,9 @@ def __init__(self, agents: AgentsResource) -> None:
11341309
self.update_status = to_raw_response_wrapper(
11351310
agents.update_status,
11361311
)
1312+
self.wait_until_ready = to_raw_response_wrapper(
1313+
agents.wait_until_ready,
1314+
)
11371315

11381316
@cached_property
11391317
def api_keys(self) -> APIKeysResourceWithRawResponse:
@@ -1201,6 +1379,9 @@ def __init__(self, agents: AsyncAgentsResource) -> None:
12011379
self.update_status = async_to_raw_response_wrapper(
12021380
agents.update_status,
12031381
)
1382+
self.wait_until_ready = async_to_raw_response_wrapper(
1383+
agents.wait_until_ready,
1384+
)
12041385

12051386
@cached_property
12061387
def api_keys(self) -> AsyncAPIKeysResourceWithRawResponse:
@@ -1268,6 +1449,9 @@ def __init__(self, agents: AgentsResource) -> None:
12681449
self.update_status = to_streamed_response_wrapper(
12691450
agents.update_status,
12701451
)
1452+
self.wait_until_ready = to_streamed_response_wrapper(
1453+
agents.wait_until_ready,
1454+
)
12711455

12721456
@cached_property
12731457
def api_keys(self) -> APIKeysResourceWithStreamingResponse:
@@ -1335,6 +1519,9 @@ def __init__(self, agents: AsyncAgentsResource) -> None:
13351519
self.update_status = async_to_streamed_response_wrapper(
13361520
agents.update_status,
13371521
)
1522+
self.wait_until_ready = async_to_streamed_response_wrapper(
1523+
agents.wait_until_ready,
1524+
)
13381525

13391526
@cached_property
13401527
def api_keys(self) -> AsyncAPIKeysResourceWithStreamingResponse:

0 commit comments

Comments
 (0)