Skip to content

Commit 59ab161

Browse files
committed
feat: add wait_until_database_online helper for knowledge base database polling
- Add wait_until_database_online method to KnowledgeBasesResource and AsyncKnowledgeBasesResource - Polls knowledge base database_status until it reaches ONLINE or encounters terminal failure - Implements configurable timeout and poll_interval parameters - Add two new exception types: KnowledgeBaseDatabaseError and KnowledgeBaseDatabaseTimeoutError - Expose new method through WithRawResponse and WithStreamingResponse wrappers - Add comprehensive unit tests (8 tests: 4 sync + 4 async) covering success, timeout, failure, and validation scenarios - Follows the same pattern as agents.wait_until_ready for consistency Closes #42
1 parent dcef3d5 commit 59ab161

File tree

5 files changed

+283
-36
lines changed

5 files changed

+283
-36
lines changed

examples/agent_wait_until_ready.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,32 @@
2424
if agent_id:
2525
print(f"Agent created with ID: {agent_id}")
2626
print("Waiting for agent to be ready...")
27-
27+
2828
try:
2929
# Wait for the agent to be deployed and ready
3030
# This will poll the agent status every 5 seconds (default)
3131
# and wait up to 5 minutes (default timeout=300 seconds)
3232
ready_agent = client.agents.wait_until_ready(
3333
agent_id,
3434
poll_interval=5.0, # Check every 5 seconds
35-
timeout=300.0, # Wait up to 5 minutes
35+
timeout=300.0, # Wait up to 5 minutes
3636
)
37-
37+
3838
if ready_agent.agent and ready_agent.agent.deployment:
3939
print(f"Agent is ready! Status: {ready_agent.agent.deployment.status}")
4040
print(f"Agent URL: {ready_agent.agent.url}")
41-
41+
4242
# Now you can use the agent
4343
# ...
44-
44+
4545
except AgentDeploymentError as e:
4646
print(f"Agent deployment failed: {e}")
4747
print(f"Failed status: {e.status}")
48-
48+
4949
except AgentDeploymentTimeoutError as e:
5050
print(f"Agent deployment timed out: {e}")
5151
print(f"Agent ID: {e.agent_id}")
52-
52+
5353
except Exception as e:
5454
print(f"Unexpected error: {e}")
5555

@@ -60,37 +60,37 @@
6060

6161
async def main() -> None:
6262
async_client = AsyncGradient()
63-
63+
6464
# Create a new agent
6565
agent_response = await async_client.agents.create(
6666
name="My Async Agent",
6767
instruction="You are a helpful assistant",
6868
model_uuid="<your-model-uuid>",
6969
region="nyc1",
7070
)
71-
71+
7272
agent_id = agent_response.agent.uuid if agent_response.agent else None
73-
73+
7474
if agent_id:
7575
print(f"Agent created with ID: {agent_id}")
7676
print("Waiting for agent to be ready...")
77-
77+
7878
try:
7979
# Wait for the agent to be deployed and ready (async)
8080
ready_agent = await async_client.agents.wait_until_ready(
8181
agent_id,
8282
poll_interval=5.0,
8383
timeout=300.0,
8484
)
85-
85+
8686
if ready_agent.agent and ready_agent.agent.deployment:
8787
print(f"Agent is ready! Status: {ready_agent.agent.deployment.status}")
8888
print(f"Agent URL: {ready_agent.agent.url}")
89-
89+
9090
except AgentDeploymentError as e:
9191
print(f"Agent deployment failed: {e}")
9292
print(f"Failed status: {e.status}")
93-
93+
9494
except AgentDeploymentTimeoutError as e:
9595
print(f"Agent deployment timed out: {e}")
9696
print(f"Agent ID: {e.agent_id}")

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+
"KnowledgeBaseDatabaseError",
19+
"KnowledgeBaseDatabaseTimeoutError",
1820
"AgentDeploymentError",
1921
"AgentDeploymentTimeoutError",
2022
]
@@ -124,3 +126,19 @@ class AgentDeploymentTimeoutError(GradientError):
124126
def __init__(self, message: str, agent_id: str) -> None:
125127
super().__init__(message)
126128
self.agent_id = agent_id
129+
130+
131+
class KnowledgeBaseDatabaseError(GradientError):
132+
"""Raised when a knowledge base database creation fails."""
133+
134+
def __init__(self, message: str, status: str) -> None:
135+
super().__init__(message)
136+
self.status = status
137+
138+
139+
class KnowledgeBaseDatabaseTimeoutError(GradientError):
140+
"""Raised when waiting for a knowledge base database creation times out."""
141+
142+
def __init__(self, message: str, knowledge_base_id: str) -> None:
143+
super().__init__(message)
144+
self.knowledge_base_id = knowledge_base_id

src/gradient/resources/knowledge_bases/knowledge_bases.py

Lines changed: 108 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
from typing import Iterable
67

78
import httpx
@@ -330,6 +331,56 @@ def delete(
330331
cast_to=KnowledgeBaseDeleteResponse,
331332
)
332333

334+
def wait_until_database_online(
335+
self,
336+
uuid: str,
337+
*,
338+
timeout: float = 300.0,
339+
poll_interval: float = 5.0,
340+
extra_headers: Headers | None = None,
341+
extra_query: Query | None = None,
342+
extra_body: Body | None = None,
343+
) -> KnowledgeBaseRetrieveResponse:
344+
"""Wait for a knowledge base's associated database to reach ONLINE.
345+
346+
This polls `retrieve` until `database_status` equals "ONLINE", or raises
347+
on terminal failure or timeout.
348+
"""
349+
from ..._exceptions import KnowledgeBaseDatabaseError, KnowledgeBaseDatabaseTimeoutError
350+
351+
if not uuid:
352+
raise ValueError(f"Expected a non-empty value for `uuid` but received {uuid!r}")
353+
354+
start_time = time.time()
355+
356+
while True:
357+
kb_response = self.retrieve(
358+
uuid, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body
359+
)
360+
361+
status = kb_response.database_status if kb_response else None
362+
363+
# Success
364+
if status == "ONLINE":
365+
return kb_response
366+
367+
# Failure cases - treat some terminal statuses as failures
368+
if status in ("DECOMMISSIONED", "UNHEALTHY"):
369+
raise KnowledgeBaseDatabaseError(
370+
f"Knowledge base database creation failed with status: {status}", status=status
371+
)
372+
373+
# Timeout
374+
elapsed_time = time.time() - start_time
375+
if elapsed_time >= timeout:
376+
current_status = status or "UNKNOWN"
377+
raise KnowledgeBaseDatabaseTimeoutError(
378+
f"Knowledge base database did not reach ONLINE within {timeout} seconds. Current status: {current_status}",
379+
knowledge_base_id=uuid,
380+
)
381+
382+
time.sleep(poll_interval)
383+
333384

334385
class AsyncKnowledgeBasesResource(AsyncAPIResource):
335386
@cached_property
@@ -618,6 +669,51 @@ async def delete(
618669
cast_to=KnowledgeBaseDeleteResponse,
619670
)
620671

672+
async def wait_until_database_online(
673+
self,
674+
uuid: str,
675+
*,
676+
timeout: float = 300.0,
677+
poll_interval: float = 5.0,
678+
extra_headers: Headers | None = None,
679+
extra_query: Query | None = None,
680+
extra_body: Body | None = None,
681+
) -> KnowledgeBaseRetrieveResponse:
682+
"""Async version of `wait_until_database_online`."""
683+
import asyncio
684+
685+
from ..._exceptions import KnowledgeBaseDatabaseError, KnowledgeBaseDatabaseTimeoutError
686+
687+
if not uuid:
688+
raise ValueError(f"Expected a non-empty value for `uuid` but received {uuid!r}")
689+
690+
start_time = time.time()
691+
692+
while True:
693+
kb_response = await self.retrieve(
694+
uuid, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body
695+
)
696+
697+
status = kb_response.database_status if kb_response else None
698+
699+
if status == "ONLINE":
700+
return kb_response
701+
702+
if status in ("DECOMMISSIONED", "UNHEALTHY"):
703+
raise KnowledgeBaseDatabaseError(
704+
f"Knowledge base database creation failed with status: {status}", status=status
705+
)
706+
707+
elapsed_time = time.time() - start_time
708+
if elapsed_time >= timeout:
709+
current_status = status or "UNKNOWN"
710+
raise KnowledgeBaseDatabaseTimeoutError(
711+
f"Knowledge base database did not reach ONLINE within {timeout} seconds. Current status: {current_status}",
712+
knowledge_base_id=uuid,
713+
)
714+
715+
await asyncio.sleep(poll_interval)
716+
621717

622718
class KnowledgeBasesResourceWithRawResponse:
623719
def __init__(self, knowledge_bases: KnowledgeBasesResource) -> None:
@@ -638,6 +734,9 @@ def __init__(self, knowledge_bases: KnowledgeBasesResource) -> None:
638734
self.delete = to_raw_response_wrapper(
639735
knowledge_bases.delete,
640736
)
737+
self.wait_until_database_online = to_raw_response_wrapper(
738+
knowledge_bases.wait_until_database_online,
739+
)
641740

642741
@cached_property
643742
def data_sources(self) -> DataSourcesResourceWithRawResponse:
@@ -667,6 +766,9 @@ def __init__(self, knowledge_bases: AsyncKnowledgeBasesResource) -> None:
667766
self.delete = async_to_raw_response_wrapper(
668767
knowledge_bases.delete,
669768
)
769+
self.wait_until_database_online = async_to_raw_response_wrapper(
770+
knowledge_bases.wait_until_database_online,
771+
)
670772

671773
@cached_property
672774
def data_sources(self) -> AsyncDataSourcesResourceWithRawResponse:
@@ -696,6 +798,9 @@ def __init__(self, knowledge_bases: KnowledgeBasesResource) -> None:
696798
self.delete = to_streamed_response_wrapper(
697799
knowledge_bases.delete,
698800
)
801+
self.wait_until_database_online = to_streamed_response_wrapper(
802+
knowledge_bases.wait_until_database_online,
803+
)
699804

700805
@cached_property
701806
def data_sources(self) -> DataSourcesResourceWithStreamingResponse:
@@ -725,6 +830,9 @@ def __init__(self, knowledge_bases: AsyncKnowledgeBasesResource) -> None:
725830
self.delete = async_to_streamed_response_wrapper(
726831
knowledge_bases.delete,
727832
)
833+
self.wait_until_database_online = async_to_streamed_response_wrapper(
834+
knowledge_bases.wait_until_database_online,
835+
)
728836

729837
@cached_property
730838
def data_sources(self) -> AsyncDataSourcesResourceWithStreamingResponse:

0 commit comments

Comments
 (0)