modus-sdk 0.2.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.
- modus/__init__.py +144 -0
- modus/_auth.py +111 -0
- modus/_chat.py +411 -0
- modus/_client.py +240 -0
- modus/_config.py +89 -0
- modus/_deprecation.py +29 -0
- modus/_exceptions.py +149 -0
- modus/_generated/__init__.py +341 -0
- modus/_generated/models.py +3182 -0
- modus/_generated/operations.py +891 -0
- modus/_http.py +471 -0
- modus/_openapi_invoke.py +137 -0
- modus/_pagination.py +175 -0
- modus/_query.py +38 -0
- modus/_retry.py +123 -0
- modus/_streaming.py +115 -0
- modus/_validation.py +61 -0
- modus/_version.py +24 -0
- modus/management/__init__.py +74 -0
- modus/management/_client.py +168 -0
- modus/management/resources/__init__.py +21 -0
- modus/management/resources/_access_config_write.py +31 -0
- modus/management/resources/context/__init__.py +0 -0
- modus/management/resources/context/_content_merge.py +128 -0
- modus/management/resources/context/context.py +362 -0
- modus/management/resources/context/custom_items.py +318 -0
- modus/management/resources/context/items.py +234 -0
- modus/management/resources/organization.py +52 -0
- modus/management/resources/scopes/__init__.py +0 -0
- modus/management/resources/scopes/_update_body.py +38 -0
- modus/management/resources/scopes/conversations.py +177 -0
- modus/management/resources/scopes/evaluations.py +142 -0
- modus/management/resources/scopes/memories.py +273 -0
- modus/management/resources/scopes/scopes.py +764 -0
- modus/management/resources/scopes/supervision.py +85 -0
- modus/management/resources/usage.py +92 -0
- modus/management/resources/users.py +56 -0
- modus/management/resources/workflows/__init__.py +0 -0
- modus/management/resources/workflows/_update_body.py +29 -0
- modus/management/resources/workflows/interfaces.py +136 -0
- modus/management/resources/workflows/workflows.py +647 -0
- modus/management/types/__init__.py +57 -0
- modus/management/types/_guardrails_access.py +33 -0
- modus/management/types/_serialize.py +52 -0
- modus/management/types/agent_selection.py +66 -0
- modus/management/types/agents.py +5 -0
- modus/management/types/context.py +25 -0
- modus/management/types/conversations.py +5 -0
- modus/management/types/evaluations.py +25 -0
- modus/management/types/json_blob.py +51 -0
- modus/management/types/memories.py +69 -0
- modus/management/types/ownership.py +9 -0
- modus/management/types/skills.py +5 -0
- modus/management/types/toolset.py +236 -0
- modus/management/types/triggers.py +284 -0
- modus/management/types/usage.py +13 -0
- modus/management/types/workflow.py +45 -0
- modus/py.typed +0 -0
- modus/resources/__init__.py +21 -0
- modus/resources/connections/__init__.py +3 -0
- modus/resources/connections/connections.py +189 -0
- modus/resources/context/__init__.py +3 -0
- modus/resources/context/context.py +48 -0
- modus/resources/context/items.py +410 -0
- modus/resources/modus/__init__.py +3 -0
- modus/resources/modus/conversations.py +144 -0
- modus/resources/modus/modus.py +138 -0
- modus/resources/scopes/__init__.py +3 -0
- modus/resources/scopes/conversations.py +213 -0
- modus/resources/scopes/scopes.py +431 -0
- modus/resources/suggestions/__init__.py +3 -0
- modus/resources/suggestions/suggestions.py +207 -0
- modus/resources/workflows/__init__.py +15 -0
- modus/resources/workflows/runs.py +833 -0
- modus/resources/workflows/workflow_actions.py +122 -0
- modus/resources/workflows/workflows.py +358 -0
- modus/types/__init__.py +80 -0
- modus/types/agent_runs.py +70 -0
- modus/types/agents.py +17 -0
- modus/types/chat.py +9 -0
- modus/types/compose.py +21 -0
- modus/types/connections.py +9 -0
- modus/types/context.py +28 -0
- modus/types/context_accessors.py +89 -0
- modus/types/context_compose.py +18 -0
- modus/types/context_values.py +21 -0
- modus/types/conversations.py +30 -0
- modus/types/json_blob.py +24 -0
- modus/types/pagination.py +142 -0
- modus/types/runs.py +71 -0
- modus/types/skills.py +16 -0
- modus/types/suggestions.py +16 -0
- modus/types/tool_calls.py +34 -0
- modus/types/views.py +6 -0
- modus_sdk-0.2.0.dist-info/METADATA +225 -0
- modus_sdk-0.2.0.dist-info/RECORD +98 -0
- modus_sdk-0.2.0.dist-info/WHEEL +4 -0
- modus_sdk-0.2.0.dist-info/licenses/LICENSE +21 -0
modus/_client.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Top-level client classes.
|
|
3
|
+
|
|
4
|
+
Modus — synchronous client for scripts, notebooks, data pipelines
|
|
5
|
+
AsyncModus — asynchronous client for FastAPI, async workflows
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Mapping, Optional, Union
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from ._auth import (
|
|
15
|
+
resolve_agent_host,
|
|
16
|
+
resolve_api_key,
|
|
17
|
+
resolve_base_url,
|
|
18
|
+
resolve_max_retries,
|
|
19
|
+
resolve_timeout,
|
|
20
|
+
)
|
|
21
|
+
from ._config import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, ModusConfig
|
|
22
|
+
from ._http import AsyncHttpClient, HttpClient
|
|
23
|
+
from .resources import (
|
|
24
|
+
AsyncConnectionsResource,
|
|
25
|
+
AsyncContextResource,
|
|
26
|
+
AsyncModusResource,
|
|
27
|
+
AsyncScopesResource,
|
|
28
|
+
AsyncSuggestionsResource,
|
|
29
|
+
AsyncWorkflowsResource,
|
|
30
|
+
ConnectionsResource,
|
|
31
|
+
ContextResource,
|
|
32
|
+
ModusResource,
|
|
33
|
+
ScopesResource,
|
|
34
|
+
SuggestionsResource,
|
|
35
|
+
WorkflowsResource,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Modus:
|
|
40
|
+
"""
|
|
41
|
+
Synchronous Modus API client.
|
|
42
|
+
|
|
43
|
+
Instantiate once and reuse across your script or application.
|
|
44
|
+
The client is thread-safe for concurrent GET requests.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
api_key: Your Modus API key. If omitted, reads from the
|
|
48
|
+
MODUS_API_KEY environment variable.
|
|
49
|
+
base_url: API origin without a path suffix (default
|
|
50
|
+
https://api.modus.com).
|
|
51
|
+
agent_host: Agent host origin for streaming runs (e.g.
|
|
52
|
+
https://agent.modus.com). If omitted, reads from the
|
|
53
|
+
MODUS_AGENT_HOST environment variable. When not set,
|
|
54
|
+
streaming falls back to ``base_url``.
|
|
55
|
+
base_urls: Optional service-specific API origins.
|
|
56
|
+
timeout: Request timeout as seconds or httpx.Timeout. Default is
|
|
57
|
+
5s connect and 300s read.
|
|
58
|
+
max_retries: Number of automatic retries on 429/5xx (default 2).
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
import modus
|
|
62
|
+
|
|
63
|
+
client = modus.Modus(api_key="modus_xxx")
|
|
64
|
+
|
|
65
|
+
for scope in client.scopes.list():
|
|
66
|
+
print(scope.name)
|
|
67
|
+
|
|
68
|
+
# Write operations use the management client in the same package:
|
|
69
|
+
from modus.management import ModusManagement
|
|
70
|
+
|
|
71
|
+
mgmt = ModusManagement(api_key="modus_xxx")
|
|
72
|
+
mgmt.context.create_note(
|
|
73
|
+
title="Fiscal Year",
|
|
74
|
+
content="Runs Feb 1 – Jan 31.",
|
|
75
|
+
)
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
api_key: Optional[str] = None,
|
|
81
|
+
*,
|
|
82
|
+
base_url: Optional[str] = None,
|
|
83
|
+
agent_host: Optional[str] = None,
|
|
84
|
+
base_urls: Optional[Mapping[str, str]] = None,
|
|
85
|
+
timeout: Union[float, httpx.Timeout, None] = None,
|
|
86
|
+
max_retries: Optional[int] = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
resolved_key = resolve_api_key(api_key)
|
|
89
|
+
resolved_url = resolve_base_url(base_url) or DEFAULT_BASE_URL
|
|
90
|
+
resolved_agent_host = resolve_agent_host(agent_host)
|
|
91
|
+
_t = resolve_timeout(timeout)
|
|
92
|
+
resolved_timeout = _t if _t is not None else DEFAULT_TIMEOUT
|
|
93
|
+
resolved_retries = resolve_max_retries(max_retries)
|
|
94
|
+
if resolved_retries is None:
|
|
95
|
+
resolved_retries = DEFAULT_MAX_RETRIES
|
|
96
|
+
self._config = ModusConfig(
|
|
97
|
+
api_key=resolved_key,
|
|
98
|
+
base_url=resolved_url,
|
|
99
|
+
timeout=resolved_timeout,
|
|
100
|
+
max_retries=resolved_retries,
|
|
101
|
+
agent_host=resolved_agent_host,
|
|
102
|
+
base_urls=base_urls or {},
|
|
103
|
+
)
|
|
104
|
+
self._http = HttpClient(self._config)
|
|
105
|
+
# Dedicated HTTP client for streaming to the agent host.
|
|
106
|
+
# When agent_host is not configured, _agent_http shares the same
|
|
107
|
+
# base_url as _http so streaming falls back gracefully.
|
|
108
|
+
if resolved_agent_host is not None:
|
|
109
|
+
_agent_config = ModusConfig(
|
|
110
|
+
api_key=resolved_key,
|
|
111
|
+
base_url=resolved_agent_host,
|
|
112
|
+
timeout=resolved_timeout,
|
|
113
|
+
max_retries=resolved_retries,
|
|
114
|
+
)
|
|
115
|
+
self._agent_http: HttpClient = HttpClient(_agent_config)
|
|
116
|
+
else:
|
|
117
|
+
self._agent_http = self._http
|
|
118
|
+
|
|
119
|
+
self.scopes: ScopesResource = ScopesResource(self._http, self._agent_http, self._config)
|
|
120
|
+
self.modus: ModusResource = ModusResource(self._http, self._agent_http, self._config)
|
|
121
|
+
self.workflows: WorkflowsResource = WorkflowsResource(
|
|
122
|
+
self._http, self._agent_http, self._config
|
|
123
|
+
)
|
|
124
|
+
self.context: ContextResource = ContextResource(self._http, self._config)
|
|
125
|
+
self.connections: ConnectionsResource = ConnectionsResource(self._http, self._config)
|
|
126
|
+
self.suggestions: SuggestionsResource = SuggestionsResource(self._http, self._config)
|
|
127
|
+
|
|
128
|
+
def close(self) -> None:
|
|
129
|
+
"""Close the underlying HTTP connection pool."""
|
|
130
|
+
self._http.close()
|
|
131
|
+
if self._agent_http is not self._http:
|
|
132
|
+
self._agent_http.close()
|
|
133
|
+
|
|
134
|
+
def __enter__(self) -> "Modus":
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
def __exit__(self, *args: object) -> None:
|
|
138
|
+
self.close()
|
|
139
|
+
|
|
140
|
+
def __repr__(self) -> str:
|
|
141
|
+
return f"Modus(base_url={self._config.base_url!r})"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class AsyncModus:
|
|
145
|
+
"""
|
|
146
|
+
Asynchronous Modus API client.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
api_key: Your Modus API key. If omitted, reads from MODUS_API_KEY.
|
|
150
|
+
base_url: API origin without a path suffix (default
|
|
151
|
+
https://api.modus.com).
|
|
152
|
+
agent_host: Agent host origin for streaming runs (e.g.
|
|
153
|
+
https://agent.modus.com). If omitted, reads from the
|
|
154
|
+
MODUS_AGENT_HOST environment variable. When not set,
|
|
155
|
+
streaming falls back to ``base_url``.
|
|
156
|
+
base_urls: Optional service-specific API origins.
|
|
157
|
+
timeout: Request timeout as seconds or httpx.Timeout. Default is
|
|
158
|
+
5s connect and 300s read.
|
|
159
|
+
max_retries: Retries on 429/5xx (default 2).
|
|
160
|
+
|
|
161
|
+
Example:
|
|
162
|
+
import modus
|
|
163
|
+
|
|
164
|
+
client = modus.AsyncModus(api_key="modus_xxx")
|
|
165
|
+
|
|
166
|
+
async def main():
|
|
167
|
+
page = await client.scopes.list()
|
|
168
|
+
for scope in page:
|
|
169
|
+
print(scope.name)
|
|
170
|
+
await client.aclose()
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def __init__(
|
|
174
|
+
self,
|
|
175
|
+
api_key: Optional[str] = None,
|
|
176
|
+
*,
|
|
177
|
+
base_url: Optional[str] = None,
|
|
178
|
+
agent_host: Optional[str] = None,
|
|
179
|
+
base_urls: Optional[Mapping[str, str]] = None,
|
|
180
|
+
timeout: Union[float, httpx.Timeout, None] = None,
|
|
181
|
+
max_retries: Optional[int] = None,
|
|
182
|
+
) -> None:
|
|
183
|
+
resolved_key = resolve_api_key(api_key)
|
|
184
|
+
resolved_url = resolve_base_url(base_url) or DEFAULT_BASE_URL
|
|
185
|
+
resolved_agent_host = resolve_agent_host(agent_host)
|
|
186
|
+
_t = resolve_timeout(timeout)
|
|
187
|
+
resolved_timeout = _t if _t is not None else DEFAULT_TIMEOUT
|
|
188
|
+
resolved_retries = resolve_max_retries(max_retries)
|
|
189
|
+
if resolved_retries is None:
|
|
190
|
+
resolved_retries = DEFAULT_MAX_RETRIES
|
|
191
|
+
self._config = ModusConfig(
|
|
192
|
+
api_key=resolved_key,
|
|
193
|
+
base_url=resolved_url,
|
|
194
|
+
timeout=resolved_timeout,
|
|
195
|
+
max_retries=resolved_retries,
|
|
196
|
+
agent_host=resolved_agent_host,
|
|
197
|
+
base_urls=base_urls or {},
|
|
198
|
+
)
|
|
199
|
+
self._http = AsyncHttpClient(self._config)
|
|
200
|
+
# Dedicated HTTP client for streaming to the agent host.
|
|
201
|
+
if resolved_agent_host is not None:
|
|
202
|
+
_agent_config = ModusConfig(
|
|
203
|
+
api_key=resolved_key,
|
|
204
|
+
base_url=resolved_agent_host,
|
|
205
|
+
timeout=resolved_timeout,
|
|
206
|
+
max_retries=resolved_retries,
|
|
207
|
+
)
|
|
208
|
+
self._agent_http: AsyncHttpClient = AsyncHttpClient(_agent_config)
|
|
209
|
+
else:
|
|
210
|
+
self._agent_http = self._http
|
|
211
|
+
|
|
212
|
+
self.scopes: AsyncScopesResource = AsyncScopesResource(
|
|
213
|
+
self._http, self._agent_http, self._config
|
|
214
|
+
)
|
|
215
|
+
self.modus: AsyncModusResource = AsyncModusResource(self._http, self._agent_http, self._config)
|
|
216
|
+
self.workflows: AsyncWorkflowsResource = AsyncWorkflowsResource(
|
|
217
|
+
self._http, self._agent_http, self._config
|
|
218
|
+
)
|
|
219
|
+
self.context: AsyncContextResource = AsyncContextResource(self._http, self._config)
|
|
220
|
+
self.connections: AsyncConnectionsResource = AsyncConnectionsResource(
|
|
221
|
+
self._http, self._config
|
|
222
|
+
)
|
|
223
|
+
self.suggestions: AsyncSuggestionsResource = AsyncSuggestionsResource(
|
|
224
|
+
self._http, self._config
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
async def aclose(self) -> None:
|
|
228
|
+
"""Close the underlying async HTTP connection pool."""
|
|
229
|
+
await self._http.aclose()
|
|
230
|
+
if self._agent_http is not self._http:
|
|
231
|
+
await self._agent_http.aclose()
|
|
232
|
+
|
|
233
|
+
async def __aenter__(self) -> "AsyncModus":
|
|
234
|
+
return self
|
|
235
|
+
|
|
236
|
+
async def __aexit__(self, *args: object) -> None:
|
|
237
|
+
await self.aclose()
|
|
238
|
+
|
|
239
|
+
def __repr__(self) -> str:
|
|
240
|
+
return f"AsyncModus(base_url={self._config.base_url!r})"
|
modus/_config.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SDK configuration.
|
|
3
|
+
|
|
4
|
+
ModusConfig holds all settings for a client instance. It is created once
|
|
5
|
+
during Modus.__init__ and passed down to the HTTP client and all resources.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from types import MappingProxyType
|
|
12
|
+
from typing import Mapping, Optional, Union
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
# Defaults — override via Modus(base_url=..., timeout=..., max_retries=...)
|
|
17
|
+
DEFAULT_BASE_URL = "https://api.modus.com"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _default_timeout() -> httpx.Timeout:
|
|
21
|
+
"""Factory function to create the default timeout object."""
|
|
22
|
+
return httpx.Timeout(
|
|
23
|
+
connect=5.0,
|
|
24
|
+
read=300.0,
|
|
25
|
+
write=10.0,
|
|
26
|
+
pool=5.0,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
DEFAULT_TIMEOUT: Union[float, httpx.Timeout] = _default_timeout()
|
|
31
|
+
DEFAULT_MAX_RETRIES = 2 # retried on 429 + 5xx only
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _normalize_base_url(value: str) -> str:
|
|
35
|
+
return value.rstrip("/")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _normalize_base_urls(value: Mapping[str, str]) -> Mapping[str, str]:
|
|
39
|
+
return MappingProxyType(
|
|
40
|
+
{service: _normalize_base_url(base_url) for service, base_url in value.items()}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class ModusConfig:
|
|
46
|
+
api_key: str
|
|
47
|
+
base_url: str = DEFAULT_BASE_URL
|
|
48
|
+
timeout: Union[float, httpx.Timeout] = field(default_factory=_default_timeout)
|
|
49
|
+
max_retries: int = DEFAULT_MAX_RETRIES
|
|
50
|
+
agent_host: Optional[str] = None
|
|
51
|
+
base_urls: Mapping[str, str] = field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
def __post_init__(self) -> None:
|
|
54
|
+
# Reject obviously-bad max_retries early — negative/non-int would later
|
|
55
|
+
# bottom out in `range(max_retries + 1)` yielding zero attempts and a
|
|
56
|
+
# confusing "unreachable" RuntimeError from the retry loop.
|
|
57
|
+
if not isinstance(self.max_retries, int) or isinstance(self.max_retries, bool) or self.max_retries < 0:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"max_retries must be a non-negative int, got {self.max_retries!r}"
|
|
60
|
+
)
|
|
61
|
+
# frozen=True prevents self.base_url = ... — use object.__setattr__
|
|
62
|
+
object.__setattr__(self, "base_url", _normalize_base_url(self.base_url))
|
|
63
|
+
if self.agent_host is not None:
|
|
64
|
+
object.__setattr__(self, "agent_host", _normalize_base_url(self.agent_host))
|
|
65
|
+
object.__setattr__(self, "base_urls", _normalize_base_urls(self.base_urls))
|
|
66
|
+
|
|
67
|
+
def __repr__(self) -> str:
|
|
68
|
+
masked = f"{self.api_key[:10]}***" if len(self.api_key) > 10 else "***"
|
|
69
|
+
return (
|
|
70
|
+
f"ModusConfig(api_key={masked!r}, base_url={self.base_url!r}, "
|
|
71
|
+
f"timeout={self.timeout!r}, max_retries={self.max_retries!r})"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def resolve_service_base_url(
|
|
76
|
+
config: ModusConfig,
|
|
77
|
+
service: str,
|
|
78
|
+
server_url: Optional[str],
|
|
79
|
+
) -> str:
|
|
80
|
+
"""Return the origin for an operation's owning public service."""
|
|
81
|
+
if service == "modus-api":
|
|
82
|
+
return config.base_url
|
|
83
|
+
if service in config.base_urls:
|
|
84
|
+
return config.base_urls[service]
|
|
85
|
+
if service == "agent-service" and config.agent_host:
|
|
86
|
+
return config.agent_host
|
|
87
|
+
if server_url:
|
|
88
|
+
return _normalize_base_url(server_url)
|
|
89
|
+
return config.base_url
|
modus/_deprecation.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Shared deprecation warnings for public SDK surfaces.
|
|
2
|
+
|
|
3
|
+
Call :func:`warn_deprecated` at the start of any public method that still
|
|
4
|
+
invokes an OpenAPI operation marked ``deprecated: true``. Release N keeps the
|
|
5
|
+
method working and warns; release N+1 removes the operation and the method.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def warn_deprecated(*, old: str, new: str, stacklevel: int = 2) -> None:
|
|
14
|
+
"""Emit a :class:`DeprecationWarning` naming the replacement surface.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
old:
|
|
19
|
+
The deprecated public name (method, attribute, or path segment).
|
|
20
|
+
new:
|
|
21
|
+
The replacement callers should use.
|
|
22
|
+
stacklevel:
|
|
23
|
+
Passed to :func:`warnings.warn` (default 2 points at the caller).
|
|
24
|
+
"""
|
|
25
|
+
warnings.warn(
|
|
26
|
+
f"{old} is deprecated and will be removed in the next release; use {new} instead.",
|
|
27
|
+
DeprecationWarning,
|
|
28
|
+
stacklevel=stacklevel,
|
|
29
|
+
)
|
modus/_exceptions.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modus SDK exception hierarchy.
|
|
3
|
+
|
|
4
|
+
All exceptions raised by the SDK are subclasses of ModusError.
|
|
5
|
+
HTTP errors are mapped from status codes; run errors are raised from SSE events.
|
|
6
|
+
|
|
7
|
+
HTTP mapping:
|
|
8
|
+
401 -> AuthenticationError
|
|
9
|
+
403 -> PermissionDeniedError
|
|
10
|
+
404 -> NotFoundError
|
|
11
|
+
409 -> ConflictError
|
|
12
|
+
422 -> UnprocessableError
|
|
13
|
+
429 -> RateLimitError
|
|
14
|
+
5xx -> InternalServerError
|
|
15
|
+
|
|
16
|
+
SSE event mapping:
|
|
17
|
+
{ type: "error" } -> ModusError (raised from run/stream)
|
|
18
|
+
{ type: "cancelled" } -> RunCancelledError
|
|
19
|
+
{ type: "stream_timeout" } -> StreamTimeoutError
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any, Mapping, Optional
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ModusError(Exception):
|
|
28
|
+
"""Base class for all Modus SDK errors."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
message: str,
|
|
33
|
+
*,
|
|
34
|
+
status_code: Optional[int] = None,
|
|
35
|
+
request_id: Optional[str] = None,
|
|
36
|
+
response_headers: Optional[Mapping[str, str]] = None,
|
|
37
|
+
body: Optional[str] = None,
|
|
38
|
+
code: Optional[str] = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
super().__init__(message)
|
|
41
|
+
self.message = message
|
|
42
|
+
self.status_code = status_code
|
|
43
|
+
self.request_id = request_id
|
|
44
|
+
self.response_headers = dict(response_headers) if response_headers else None
|
|
45
|
+
self.body = body
|
|
46
|
+
self.code = code
|
|
47
|
+
|
|
48
|
+
def __repr__(self) -> str:
|
|
49
|
+
return (
|
|
50
|
+
f"{self.__class__.__name__}("
|
|
51
|
+
f"message={self.message!r}, "
|
|
52
|
+
f"status_code={self.status_code!r}, "
|
|
53
|
+
f"request_id={self.request_id!r})"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class APIConnectionError(ModusError):
|
|
58
|
+
"""
|
|
59
|
+
Network-level failure (ConnectError, ReadTimeout, etc.) after all
|
|
60
|
+
automatic retries are exhausted.
|
|
61
|
+
"""
|
|
62
|
+
def __init__(self, message: str) -> None:
|
|
63
|
+
super().__init__(message)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AuthenticationError(ModusError):
|
|
67
|
+
"""
|
|
68
|
+
The API key is missing, invalid, or expired (HTTP 401).
|
|
69
|
+
|
|
70
|
+
Check that MODUS_API_KEY is set correctly, or pass api_key= explicitly.
|
|
71
|
+
Create a token at app.modus.com → Settings → API Tokens.
|
|
72
|
+
"""
|
|
73
|
+
def __init__(self, message: str, **kwargs: Any) -> None:
|
|
74
|
+
super().__init__(message, status_code=401, **kwargs)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class PermissionDeniedError(ModusError):
|
|
78
|
+
"""Raised when the API key does not have permission for the requested operation (HTTP 403)."""
|
|
79
|
+
def __init__(self, message: str, **kwargs: Any) -> None:
|
|
80
|
+
super().__init__(message, status_code=403, **kwargs)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class NotFoundError(ModusError):
|
|
84
|
+
"""The requested resource does not exist (HTTP 404)."""
|
|
85
|
+
def __init__(self, message: str, **kwargs: Any) -> None:
|
|
86
|
+
super().__init__(message, status_code=404, **kwargs)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ConflictError(ModusError):
|
|
90
|
+
"""
|
|
91
|
+
Idempotency conflict — a run with this ID already exists with a different
|
|
92
|
+
body (HTTP 409). Use a new idempotency key or omit it to auto-generate.
|
|
93
|
+
"""
|
|
94
|
+
def __init__(self, message: str, **kwargs: Any) -> None:
|
|
95
|
+
super().__init__(message, status_code=409, **kwargs)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class UnprocessableError(ModusError):
|
|
99
|
+
"""
|
|
100
|
+
Request validation failed (HTTP 422).
|
|
101
|
+
The `errors` attribute contains the full validation error detail.
|
|
102
|
+
"""
|
|
103
|
+
def __init__(self, message: str, *, errors: Optional[Any] = None, **kwargs: Any) -> None:
|
|
104
|
+
super().__init__(message, status_code=422, **kwargs)
|
|
105
|
+
self.errors = errors
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class RateLimitError(ModusError):
|
|
109
|
+
"""
|
|
110
|
+
Rate limit exceeded (HTTP 429).
|
|
111
|
+
|
|
112
|
+
The `retry_after` attribute contains the number of seconds to wait
|
|
113
|
+
before retrying, if the server provided a Retry-After header.
|
|
114
|
+
The SDK retries automatically — this error only surfaces after all
|
|
115
|
+
retry attempts are exhausted.
|
|
116
|
+
"""
|
|
117
|
+
def __init__(self, message: str, *, retry_after: Optional[int] = None, **kwargs: Any) -> None:
|
|
118
|
+
super().__init__(message, status_code=429, **kwargs)
|
|
119
|
+
self.retry_after = retry_after
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class InternalServerError(ModusError):
|
|
123
|
+
"""
|
|
124
|
+
The server encountered an internal error (HTTP 5xx).
|
|
125
|
+
The SDK retries automatically — this error only surfaces after all
|
|
126
|
+
retry attempts are exhausted.
|
|
127
|
+
"""
|
|
128
|
+
def __init__(self, message: str, *, status_code: int, **kwargs: Any) -> None:
|
|
129
|
+
super().__init__(message, status_code=status_code, **kwargs)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# Run-specific errors (raised from SSE events, not HTTP status codes)
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class RunCancelledError(ModusError):
|
|
138
|
+
"""Raised when a chat stream is cancelled before completing."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, message: str = "Run was cancelled.") -> None:
|
|
141
|
+
super().__init__(message)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class StreamTimeoutError(ModusError):
|
|
145
|
+
"""Raised when a chat stream times out before completion."""
|
|
146
|
+
|
|
147
|
+
def __init__(self, message: str = "Stream timed out.") -> None:
|
|
148
|
+
super().__init__(message)
|
|
149
|
+
|