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.
@@ -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()
@@ -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
@@ -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)