omniai-kit 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omniai/__init__.py +117 -0
- omniai/_base.py +386 -0
- omniai/_constants.py +39 -0
- omniai/_version.py +4 -0
- omniai/client.py +316 -0
- omniai/exceptions.py +113 -0
- omniai/py.typed +1 -0
- omniai/resources/__init__.py +26 -0
- omniai/resources/chat.py +403 -0
- omniai/resources/livekit.py +71 -0
- omniai/resources/models.py +63 -0
- omniai/resources/router.py +1040 -0
- omniai/resources/tasks.py +171 -0
- omniai/types/__init__.py +27 -0
- omniai/types/chat.py +48 -0
- omniai/types/livekit.py +17 -0
- omniai/types/models.py +30 -0
- omniai/types/tasks.py +208 -0
- omniai_kit-1.0.0.dist-info/METADATA +270 -0
- omniai_kit-1.0.0.dist-info/RECORD +23 -0
- omniai_kit-1.0.0.dist-info/WHEEL +5 -0
- omniai_kit-1.0.0.dist-info/licenses/LICENSE +21 -0
- omniai_kit-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Background tasks resource for OmniAI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, TypeVar, Union
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from omniai.types.tasks import TaskJob, AsyncTaskJob, TaskStatus
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from omniai._base import BaseClient, AsyncBaseClient
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TasksResource:
|
|
17
|
+
"""Synchronous background tasks resource."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, client: BaseClient):
|
|
20
|
+
self._client = client
|
|
21
|
+
|
|
22
|
+
def submit(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
model: str,
|
|
26
|
+
prompt: Optional[str] = None,
|
|
27
|
+
messages: Optional[List[Dict[str, str]]] = None,
|
|
28
|
+
response_model: Optional[Type[T]] = None,
|
|
29
|
+
tools: Optional[List[Any]] = None,
|
|
30
|
+
output_format: Optional[Union[str, Dict[str, Any]]] = None,
|
|
31
|
+
system_instruction: Optional[str] = None,
|
|
32
|
+
temperature: float = 0.1,
|
|
33
|
+
max_tokens: Optional[int] = None,
|
|
34
|
+
timeout: Optional[float] = None,
|
|
35
|
+
) -> TaskJob[T]:
|
|
36
|
+
"""Submits an asynchronous task to Celery queue and returns a TaskJob handle."""
|
|
37
|
+
payload: Dict[str, Any] = {
|
|
38
|
+
"model": model,
|
|
39
|
+
"temperature": temperature,
|
|
40
|
+
}
|
|
41
|
+
if prompt is not None:
|
|
42
|
+
payload["prompt"] = prompt
|
|
43
|
+
if messages is not None:
|
|
44
|
+
payload["messages"] = messages
|
|
45
|
+
if system_instruction is not None:
|
|
46
|
+
payload["system_instruction"] = system_instruction
|
|
47
|
+
if max_tokens is not None:
|
|
48
|
+
payload["max_tokens"] = max_tokens
|
|
49
|
+
if tools:
|
|
50
|
+
payload["tools"] = tools
|
|
51
|
+
if output_format:
|
|
52
|
+
payload["output_format"] = output_format
|
|
53
|
+
elif response_model and issubclass(response_model, BaseModel):
|
|
54
|
+
payload["output_format"] = response_model.model_json_schema()
|
|
55
|
+
|
|
56
|
+
response = self._client.client.post(
|
|
57
|
+
"/v1/chat/completions/async",
|
|
58
|
+
json=payload,
|
|
59
|
+
timeout=timeout or self._client.timeout,
|
|
60
|
+
)
|
|
61
|
+
if response.status_code not in (200, 202):
|
|
62
|
+
self._client._handle_error_response(response)
|
|
63
|
+
|
|
64
|
+
data = response.json()
|
|
65
|
+
task_id = data.get("task_id")
|
|
66
|
+
|
|
67
|
+
return TaskJob(
|
|
68
|
+
task_id=task_id,
|
|
69
|
+
status_checker=self.get_status,
|
|
70
|
+
canceller=self.cancel,
|
|
71
|
+
response_model=response_model,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def get_status(self, task_id: str, timeout: Optional[float] = None) -> TaskStatus:
|
|
75
|
+
"""Checks status of background task."""
|
|
76
|
+
response = self._client.client.get(
|
|
77
|
+
f"/v1/chat/completions/tasks/{task_id}",
|
|
78
|
+
timeout=timeout or self._client.timeout,
|
|
79
|
+
)
|
|
80
|
+
if response.status_code != 200:
|
|
81
|
+
self._client._handle_error_response(response)
|
|
82
|
+
return TaskStatus.model_validate(response.json())
|
|
83
|
+
|
|
84
|
+
def cancel(self, task_id: str, timeout: Optional[float] = None) -> Dict[str, Any]:
|
|
85
|
+
"""Cancels a background task."""
|
|
86
|
+
response = self._client.client.delete(
|
|
87
|
+
f"/v1/chat/completions/tasks/{task_id}",
|
|
88
|
+
timeout=timeout or self._client.timeout,
|
|
89
|
+
)
|
|
90
|
+
if response.status_code != 200:
|
|
91
|
+
self._client._handle_error_response(response)
|
|
92
|
+
return response.json()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AsyncTasksResource:
|
|
96
|
+
"""Asynchronous background tasks resource."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, client: AsyncBaseClient):
|
|
99
|
+
self._client = client
|
|
100
|
+
|
|
101
|
+
async def submit(
|
|
102
|
+
self,
|
|
103
|
+
*,
|
|
104
|
+
model: str,
|
|
105
|
+
prompt: Optional[str] = None,
|
|
106
|
+
messages: Optional[List[Dict[str, str]]] = None,
|
|
107
|
+
response_model: Optional[Type[T]] = None,
|
|
108
|
+
tools: Optional[List[Any]] = None,
|
|
109
|
+
output_format: Optional[Union[str, Dict[str, Any]]] = None,
|
|
110
|
+
system_instruction: Optional[str] = None,
|
|
111
|
+
temperature: float = 0.1,
|
|
112
|
+
max_tokens: Optional[int] = None,
|
|
113
|
+
timeout: Optional[float] = None,
|
|
114
|
+
) -> AsyncTaskJob[T]:
|
|
115
|
+
"""Submits an asynchronous task to Celery queue and returns an AsyncTaskJob handle."""
|
|
116
|
+
payload: Dict[str, Any] = {
|
|
117
|
+
"model": model,
|
|
118
|
+
"temperature": temperature,
|
|
119
|
+
}
|
|
120
|
+
if prompt is not None:
|
|
121
|
+
payload["prompt"] = prompt
|
|
122
|
+
if messages is not None:
|
|
123
|
+
payload["messages"] = messages
|
|
124
|
+
if system_instruction is not None:
|
|
125
|
+
payload["system_instruction"] = system_instruction
|
|
126
|
+
if max_tokens is not None:
|
|
127
|
+
payload["max_tokens"] = max_tokens
|
|
128
|
+
if tools:
|
|
129
|
+
payload["tools"] = tools
|
|
130
|
+
if output_format:
|
|
131
|
+
payload["output_format"] = output_format
|
|
132
|
+
elif response_model and issubclass(response_model, BaseModel):
|
|
133
|
+
payload["output_format"] = response_model.model_json_schema()
|
|
134
|
+
|
|
135
|
+
response = await self._client.client.post(
|
|
136
|
+
"/v1/chat/completions/async",
|
|
137
|
+
json=payload,
|
|
138
|
+
timeout=timeout or self._client.timeout,
|
|
139
|
+
)
|
|
140
|
+
if response.status_code not in (200, 202):
|
|
141
|
+
self._client._handle_error_response(response)
|
|
142
|
+
|
|
143
|
+
data = response.json()
|
|
144
|
+
task_id = data.get("task_id")
|
|
145
|
+
|
|
146
|
+
return AsyncTaskJob(
|
|
147
|
+
task_id=task_id,
|
|
148
|
+
status_checker=self.get_status,
|
|
149
|
+
canceller=self.cancel,
|
|
150
|
+
response_model=response_model,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
async def get_status(self, task_id: str, timeout: Optional[float] = None) -> TaskStatus:
|
|
154
|
+
"""Checks status of background task asynchronously."""
|
|
155
|
+
response = await self._client.client.get(
|
|
156
|
+
f"/v1/chat/completions/tasks/{task_id}",
|
|
157
|
+
timeout=timeout or self._client.timeout,
|
|
158
|
+
)
|
|
159
|
+
if response.status_code != 200:
|
|
160
|
+
self._client._handle_error_response(response)
|
|
161
|
+
return TaskStatus.model_validate(response.json())
|
|
162
|
+
|
|
163
|
+
async def cancel(self, task_id: str, timeout: Optional[float] = None) -> Dict[str, Any]:
|
|
164
|
+
"""Cancels a background task asynchronously."""
|
|
165
|
+
response = await self._client.client.delete(
|
|
166
|
+
f"/v1/chat/completions/tasks/{task_id}",
|
|
167
|
+
timeout=timeout or self._client.timeout,
|
|
168
|
+
)
|
|
169
|
+
if response.status_code != 200:
|
|
170
|
+
self._client._handle_error_response(response)
|
|
171
|
+
return response.json()
|
omniai/types/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Unified type definitions for the OmniAI Python SDK."""
|
|
2
|
+
|
|
3
|
+
from omniai.types.chat import (
|
|
4
|
+
ChatMessage,
|
|
5
|
+
ChatCompletionChoice,
|
|
6
|
+
ChatCompletionUsage,
|
|
7
|
+
ChatCompletionResponse,
|
|
8
|
+
ChatCompletionChunk,
|
|
9
|
+
)
|
|
10
|
+
from omniai.types.models import ModelInfo, ModelList
|
|
11
|
+
from omniai.types.livekit import LiveSessionInfo
|
|
12
|
+
from omniai.types.tasks import TaskStatus, TaskJob, AsyncTaskJob, clean_json_text
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ChatMessage",
|
|
16
|
+
"ChatCompletionChoice",
|
|
17
|
+
"ChatCompletionUsage",
|
|
18
|
+
"ChatCompletionResponse",
|
|
19
|
+
"ChatCompletionChunk",
|
|
20
|
+
"ModelInfo",
|
|
21
|
+
"ModelList",
|
|
22
|
+
"LiveSessionInfo",
|
|
23
|
+
"TaskStatus",
|
|
24
|
+
"TaskJob",
|
|
25
|
+
"AsyncTaskJob",
|
|
26
|
+
"clean_json_text",
|
|
27
|
+
]
|
omniai/types/chat.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Type definitions for Chat Completions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ChatMessage(BaseModel):
|
|
10
|
+
"""A single chat message in a conversation thread."""
|
|
11
|
+
role: str = Field(default="user", description="Role of the message author (e.g. user, system, assistant)")
|
|
12
|
+
content: str = Field(..., description="The content of the message")
|
|
13
|
+
name: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChatCompletionChoice(BaseModel):
|
|
17
|
+
"""A choice in a chat completion response."""
|
|
18
|
+
index: int = 0
|
|
19
|
+
message: ChatMessage
|
|
20
|
+
finish_reason: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChatCompletionUsage(BaseModel):
|
|
24
|
+
"""Usage statistics for a completion request."""
|
|
25
|
+
prompt_tokens: int = 0
|
|
26
|
+
completion_tokens: int = 0
|
|
27
|
+
total_tokens: int = 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ChatCompletionResponse(BaseModel):
|
|
31
|
+
"""Full chat completion response from the OmniAI gateway."""
|
|
32
|
+
id: Optional[str] = None
|
|
33
|
+
object: str = "chat.completion"
|
|
34
|
+
created: Optional[int] = None
|
|
35
|
+
model: str
|
|
36
|
+
choices: List[ChatCompletionChoice] = Field(default_factory=list)
|
|
37
|
+
usage: Optional[ChatCompletionUsage] = None
|
|
38
|
+
result: Optional[Union[str, Dict[str, Any]]] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ChatCompletionChunk(BaseModel):
|
|
42
|
+
"""Streaming chunk of a chat completion."""
|
|
43
|
+
id: Optional[str] = None
|
|
44
|
+
object: str = "chat.completion.chunk"
|
|
45
|
+
created: Optional[int] = None
|
|
46
|
+
model: Optional[str] = None
|
|
47
|
+
delta: Optional[str] = None
|
|
48
|
+
finish_reason: Optional[str] = None
|
omniai/types/livekit.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Type definitions for LiveKit multimodal audio/video sessions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LiveSessionInfo(BaseModel):
|
|
10
|
+
"""LiveKit WebRTC multimodal audio/video session credentials and connection info."""
|
|
11
|
+
server_url: str = Field(..., description="WebSocket/WebRTC URL of the LiveKit server")
|
|
12
|
+
room_name: str = Field(..., description="Unique room name for the multimodal session")
|
|
13
|
+
participant_token: str = Field(..., description="JWT token for authenticating to the room")
|
|
14
|
+
participant_identity: str = Field(..., description="Unique identity assigned to the participant")
|
|
15
|
+
model_used: str = Field(default="gemini:gemini-2.5-flash", description="Underlying model configured for voice/video agent")
|
|
16
|
+
voice: str = Field(default="Aoede", description="Voice personality for realtime synthesis")
|
|
17
|
+
video_input: bool = Field(default=False, description="Whether realtime video streaming input is active")
|
omniai/types/models.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Type definitions for OmniAI Model catalog and metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ModelInfo(BaseModel):
|
|
10
|
+
"""Metadata describing an AI model supported by OmniAI."""
|
|
11
|
+
id: str
|
|
12
|
+
object: str = "model"
|
|
13
|
+
created: Optional[int] = None
|
|
14
|
+
created_at: Optional[str] = None
|
|
15
|
+
release_date: Optional[str] = None
|
|
16
|
+
owned_by: Optional[str] = None
|
|
17
|
+
provider: Optional[str] = None
|
|
18
|
+
name: Optional[str] = None
|
|
19
|
+
description: Optional[str] = None
|
|
20
|
+
context_window: Optional[int] = None
|
|
21
|
+
max_output_tokens: Optional[int] = None
|
|
22
|
+
supports_vision: bool = False
|
|
23
|
+
supports_function_calling: bool = False
|
|
24
|
+
supports_streaming: bool = True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ModelList(BaseModel):
|
|
28
|
+
"""List of supported AI models."""
|
|
29
|
+
object: str = "list"
|
|
30
|
+
data: List[ModelInfo] = Field(default_factory=list)
|
omniai/types/tasks.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Type definitions for asynchronous Celery background tasks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Callable, Generic, Optional, Type, TypeVar
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from omniai.exceptions import (
|
|
13
|
+
OmniaiExecutionError,
|
|
14
|
+
OmniaiTimeoutError,
|
|
15
|
+
OmniaiValidationError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("omniai.types.tasks")
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def clean_json_text(text: str) -> str:
|
|
24
|
+
"""Strips markdown code blocks (e.g. ```json ... ```) from model output."""
|
|
25
|
+
stripped = text.strip()
|
|
26
|
+
match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", stripped, re.IGNORECASE)
|
|
27
|
+
if match:
|
|
28
|
+
return match.group(1).strip()
|
|
29
|
+
return stripped
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TaskStatus(BaseModel):
|
|
33
|
+
"""Current status and progress of a background Celery task."""
|
|
34
|
+
task_id: str
|
|
35
|
+
status: str
|
|
36
|
+
ready: bool = False
|
|
37
|
+
result: Optional[Any] = None
|
|
38
|
+
error: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TaskJob(Generic[T]):
|
|
42
|
+
"""Synchronous handle to a background task running on the OmniAI Celery queue."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
task_id: str,
|
|
47
|
+
status_checker: Callable[[str], TaskStatus],
|
|
48
|
+
canceller: Callable[[str], dict],
|
|
49
|
+
response_model: Optional[Type[T]] = None,
|
|
50
|
+
):
|
|
51
|
+
self.task_id = task_id
|
|
52
|
+
self._status_checker = status_checker
|
|
53
|
+
self._canceller = canceller
|
|
54
|
+
self._response_model = response_model
|
|
55
|
+
self._last_status: Optional[TaskStatus] = None
|
|
56
|
+
|
|
57
|
+
def get_status(self) -> TaskStatus:
|
|
58
|
+
self._last_status = self._status_checker(self.task_id)
|
|
59
|
+
return self._last_status
|
|
60
|
+
|
|
61
|
+
def wait(
|
|
62
|
+
self,
|
|
63
|
+
timeout: float = 120.0,
|
|
64
|
+
interval: float = 1.5,
|
|
65
|
+
max_consecutive_poll_errors: int = 3,
|
|
66
|
+
) -> T:
|
|
67
|
+
start_time = time.time()
|
|
68
|
+
consecutive_errors = 0
|
|
69
|
+
|
|
70
|
+
while time.time() - start_time < timeout:
|
|
71
|
+
try:
|
|
72
|
+
status = self.get_status()
|
|
73
|
+
consecutive_errors = 0
|
|
74
|
+
except Exception as poll_err:
|
|
75
|
+
consecutive_errors += 1
|
|
76
|
+
logger.warning(
|
|
77
|
+
f"Transient error polling task {self.task_id} ({consecutive_errors}/{max_consecutive_poll_errors}): {poll_err}"
|
|
78
|
+
)
|
|
79
|
+
if consecutive_errors >= max_consecutive_poll_errors:
|
|
80
|
+
raise OmniaiExecutionError(
|
|
81
|
+
f"Failed polling task {self.task_id} after {consecutive_errors} consecutive errors: {poll_err}"
|
|
82
|
+
) from poll_err
|
|
83
|
+
time.sleep(interval)
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if status.ready:
|
|
87
|
+
if status.status == "SUCCESS":
|
|
88
|
+
raw_result = status.result
|
|
89
|
+
if self._response_model and issubclass(self._response_model, BaseModel):
|
|
90
|
+
if isinstance(raw_result, dict):
|
|
91
|
+
try:
|
|
92
|
+
return self._response_model.model_validate(raw_result)
|
|
93
|
+
except Exception as val_err:
|
|
94
|
+
raise OmniaiValidationError(
|
|
95
|
+
f"Task result validation failed: {val_err}",
|
|
96
|
+
raw_output=str(raw_result),
|
|
97
|
+
details={"error": str(val_err)},
|
|
98
|
+
) from val_err
|
|
99
|
+
elif isinstance(raw_result, str):
|
|
100
|
+
cleaned = clean_json_text(raw_result)
|
|
101
|
+
try:
|
|
102
|
+
return self._response_model.model_validate_json(cleaned)
|
|
103
|
+
except Exception as val_err:
|
|
104
|
+
raise OmniaiValidationError(
|
|
105
|
+
f"Task JSON string validation failed: {val_err}",
|
|
106
|
+
raw_output=raw_result,
|
|
107
|
+
details={"error": str(val_err)},
|
|
108
|
+
) from val_err
|
|
109
|
+
return raw_result
|
|
110
|
+
else:
|
|
111
|
+
raise OmniaiExecutionError(
|
|
112
|
+
f"Task {self.task_id} failed with status {status.status}: {status.error or 'Unknown error'}",
|
|
113
|
+
details={"status": status.status, "error": status.error},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
time.sleep(interval)
|
|
117
|
+
|
|
118
|
+
raise OmniaiTimeoutError(
|
|
119
|
+
f"Task {self.task_id} timed out after {timeout} seconds (last status: {self._last_status.status if self._last_status else 'UNKNOWN'})"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def cancel(self) -> dict:
|
|
123
|
+
return self._canceller(self.task_id)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class AsyncTaskJob(Generic[T]):
|
|
127
|
+
"""Asynchronous handle to a background task running on the OmniAI Celery queue."""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
task_id: str,
|
|
132
|
+
status_checker: Callable[[str], Any],
|
|
133
|
+
canceller: Callable[[str], Any],
|
|
134
|
+
response_model: Optional[Type[T]] = None,
|
|
135
|
+
):
|
|
136
|
+
self.task_id = task_id
|
|
137
|
+
self._status_checker = status_checker
|
|
138
|
+
self._canceller = canceller
|
|
139
|
+
self._response_model = response_model
|
|
140
|
+
self._last_status: Optional[TaskStatus] = None
|
|
141
|
+
|
|
142
|
+
async def get_status(self) -> TaskStatus:
|
|
143
|
+
self._last_status = await self._status_checker(self.task_id)
|
|
144
|
+
return self._last_status
|
|
145
|
+
|
|
146
|
+
async def wait(
|
|
147
|
+
self,
|
|
148
|
+
timeout: float = 120.0,
|
|
149
|
+
interval: float = 1.5,
|
|
150
|
+
max_consecutive_poll_errors: int = 3,
|
|
151
|
+
) -> T:
|
|
152
|
+
start_time = time.time()
|
|
153
|
+
consecutive_errors = 0
|
|
154
|
+
|
|
155
|
+
while time.time() - start_time < timeout:
|
|
156
|
+
try:
|
|
157
|
+
status = await self.get_status()
|
|
158
|
+
consecutive_errors = 0
|
|
159
|
+
except Exception as poll_err:
|
|
160
|
+
consecutive_errors += 1
|
|
161
|
+
logger.warning(
|
|
162
|
+
f"Transient error polling async task {self.task_id} ({consecutive_errors}/{max_consecutive_poll_errors}): {poll_err}"
|
|
163
|
+
)
|
|
164
|
+
if consecutive_errors >= max_consecutive_poll_errors:
|
|
165
|
+
raise OmniaiExecutionError(
|
|
166
|
+
f"Failed polling async task {self.task_id} after {consecutive_errors} consecutive errors: {poll_err}"
|
|
167
|
+
) from poll_err
|
|
168
|
+
await asyncio.sleep(interval)
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
if status.ready:
|
|
172
|
+
if status.status == "SUCCESS":
|
|
173
|
+
raw_result = status.result
|
|
174
|
+
if self._response_model and issubclass(self._response_model, BaseModel):
|
|
175
|
+
if isinstance(raw_result, dict):
|
|
176
|
+
try:
|
|
177
|
+
return self._response_model.model_validate(raw_result)
|
|
178
|
+
except Exception as val_err:
|
|
179
|
+
raise OmniaiValidationError(
|
|
180
|
+
f"Async task result validation failed: {val_err}",
|
|
181
|
+
raw_output=str(raw_result),
|
|
182
|
+
details={"error": str(val_err)},
|
|
183
|
+
) from val_err
|
|
184
|
+
elif isinstance(raw_result, str):
|
|
185
|
+
cleaned = clean_json_text(raw_result)
|
|
186
|
+
try:
|
|
187
|
+
return self._response_model.model_validate_json(cleaned)
|
|
188
|
+
except Exception as val_err:
|
|
189
|
+
raise OmniaiValidationError(
|
|
190
|
+
f"Async task JSON string validation failed: {val_err}",
|
|
191
|
+
raw_output=raw_result,
|
|
192
|
+
details={"error": str(val_err)},
|
|
193
|
+
) from val_err
|
|
194
|
+
return raw_result
|
|
195
|
+
else:
|
|
196
|
+
raise OmniaiExecutionError(
|
|
197
|
+
f"Task {self.task_id} failed with status {status.status}: {status.error or 'Unknown error'}",
|
|
198
|
+
details={"status": status.status, "error": status.error},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
await asyncio.sleep(interval)
|
|
202
|
+
|
|
203
|
+
raise OmniaiTimeoutError(
|
|
204
|
+
f"Async task {self.task_id} timed out after {timeout} seconds (last status: {self._last_status.status if self._last_status else 'UNKNOWN'})"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
async def cancel(self) -> dict:
|
|
208
|
+
return await self._canceller(self.task_id)
|