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.
Files changed (98) hide show
  1. modus/__init__.py +144 -0
  2. modus/_auth.py +111 -0
  3. modus/_chat.py +411 -0
  4. modus/_client.py +240 -0
  5. modus/_config.py +89 -0
  6. modus/_deprecation.py +29 -0
  7. modus/_exceptions.py +149 -0
  8. modus/_generated/__init__.py +341 -0
  9. modus/_generated/models.py +3182 -0
  10. modus/_generated/operations.py +891 -0
  11. modus/_http.py +471 -0
  12. modus/_openapi_invoke.py +137 -0
  13. modus/_pagination.py +175 -0
  14. modus/_query.py +38 -0
  15. modus/_retry.py +123 -0
  16. modus/_streaming.py +115 -0
  17. modus/_validation.py +61 -0
  18. modus/_version.py +24 -0
  19. modus/management/__init__.py +74 -0
  20. modus/management/_client.py +168 -0
  21. modus/management/resources/__init__.py +21 -0
  22. modus/management/resources/_access_config_write.py +31 -0
  23. modus/management/resources/context/__init__.py +0 -0
  24. modus/management/resources/context/_content_merge.py +128 -0
  25. modus/management/resources/context/context.py +362 -0
  26. modus/management/resources/context/custom_items.py +318 -0
  27. modus/management/resources/context/items.py +234 -0
  28. modus/management/resources/organization.py +52 -0
  29. modus/management/resources/scopes/__init__.py +0 -0
  30. modus/management/resources/scopes/_update_body.py +38 -0
  31. modus/management/resources/scopes/conversations.py +177 -0
  32. modus/management/resources/scopes/evaluations.py +142 -0
  33. modus/management/resources/scopes/memories.py +273 -0
  34. modus/management/resources/scopes/scopes.py +764 -0
  35. modus/management/resources/scopes/supervision.py +85 -0
  36. modus/management/resources/usage.py +92 -0
  37. modus/management/resources/users.py +56 -0
  38. modus/management/resources/workflows/__init__.py +0 -0
  39. modus/management/resources/workflows/_update_body.py +29 -0
  40. modus/management/resources/workflows/interfaces.py +136 -0
  41. modus/management/resources/workflows/workflows.py +647 -0
  42. modus/management/types/__init__.py +57 -0
  43. modus/management/types/_guardrails_access.py +33 -0
  44. modus/management/types/_serialize.py +52 -0
  45. modus/management/types/agent_selection.py +66 -0
  46. modus/management/types/agents.py +5 -0
  47. modus/management/types/context.py +25 -0
  48. modus/management/types/conversations.py +5 -0
  49. modus/management/types/evaluations.py +25 -0
  50. modus/management/types/json_blob.py +51 -0
  51. modus/management/types/memories.py +69 -0
  52. modus/management/types/ownership.py +9 -0
  53. modus/management/types/skills.py +5 -0
  54. modus/management/types/toolset.py +236 -0
  55. modus/management/types/triggers.py +284 -0
  56. modus/management/types/usage.py +13 -0
  57. modus/management/types/workflow.py +45 -0
  58. modus/py.typed +0 -0
  59. modus/resources/__init__.py +21 -0
  60. modus/resources/connections/__init__.py +3 -0
  61. modus/resources/connections/connections.py +189 -0
  62. modus/resources/context/__init__.py +3 -0
  63. modus/resources/context/context.py +48 -0
  64. modus/resources/context/items.py +410 -0
  65. modus/resources/modus/__init__.py +3 -0
  66. modus/resources/modus/conversations.py +144 -0
  67. modus/resources/modus/modus.py +138 -0
  68. modus/resources/scopes/__init__.py +3 -0
  69. modus/resources/scopes/conversations.py +213 -0
  70. modus/resources/scopes/scopes.py +431 -0
  71. modus/resources/suggestions/__init__.py +3 -0
  72. modus/resources/suggestions/suggestions.py +207 -0
  73. modus/resources/workflows/__init__.py +15 -0
  74. modus/resources/workflows/runs.py +833 -0
  75. modus/resources/workflows/workflow_actions.py +122 -0
  76. modus/resources/workflows/workflows.py +358 -0
  77. modus/types/__init__.py +80 -0
  78. modus/types/agent_runs.py +70 -0
  79. modus/types/agents.py +17 -0
  80. modus/types/chat.py +9 -0
  81. modus/types/compose.py +21 -0
  82. modus/types/connections.py +9 -0
  83. modus/types/context.py +28 -0
  84. modus/types/context_accessors.py +89 -0
  85. modus/types/context_compose.py +18 -0
  86. modus/types/context_values.py +21 -0
  87. modus/types/conversations.py +30 -0
  88. modus/types/json_blob.py +24 -0
  89. modus/types/pagination.py +142 -0
  90. modus/types/runs.py +71 -0
  91. modus/types/skills.py +16 -0
  92. modus/types/suggestions.py +16 -0
  93. modus/types/tool_calls.py +34 -0
  94. modus/types/views.py +6 -0
  95. modus_sdk-0.2.0.dist-info/METADATA +225 -0
  96. modus_sdk-0.2.0.dist-info/RECORD +98 -0
  97. modus_sdk-0.2.0.dist-info/WHEEL +4 -0
  98. modus_sdk-0.2.0.dist-info/licenses/LICENSE +21 -0
modus/_pagination.py ADDED
@@ -0,0 +1,175 @@
1
+ """
2
+ Helpers to build Page[T] / AsyncPage[T] from Modus API list responses.
3
+
4
+ Public list endpoints return resource-plural arrays and ``nextPageToken``:
5
+ { "skills": [...], "nextPageToken": "..." }
6
+
7
+ Query params use ``pageSize`` and ``pageToken``. Python resources expose
8
+ ``page_size`` / ``page_token``; ``Page.next_page_token`` maps from ``nextPageToken``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, Type, TypeVar
14
+
15
+ from ._exceptions import ModusError
16
+ from .types.pagination import AsyncPage, Page
17
+
18
+ T = TypeVar("T")
19
+
20
+
21
+ def normalize_page_token(token: Optional[str]) -> Optional[str]:
22
+ """Treat None and blank strings as no page token."""
23
+ if token is None or not str(token).strip():
24
+ return None
25
+ return token
26
+
27
+
28
+ def aip_list_params(
29
+ page_size: int,
30
+ page_token: Optional[str] = None,
31
+ **extra: Any,
32
+ ) -> Dict[str, Any]:
33
+ """Build cursor-pagination query params (pageSize, pageToken) plus optional filters."""
34
+ params: Dict[str, Any] = {"pageSize": page_size, **extra}
35
+ token = normalize_page_token(page_token)
36
+ if token is not None:
37
+ params["pageToken"] = token
38
+ return params
39
+
40
+
41
+ def _next_page_token_from_response(data: Dict[str, Any]) -> Optional[str]:
42
+ return normalize_page_token(data.get("nextPageToken"))
43
+
44
+
45
+ def _require_items_list(data: Dict[str, Any], items_key: str) -> List[Any]:
46
+ """Return ``data[items_key]`` when it is a list; otherwise raise ModusError."""
47
+ value = data.get(items_key)
48
+ if not isinstance(value, list):
49
+ raise ModusError(
50
+ f"Unexpected list response shape: {items_key!r} must be a list, "
51
+ f"got {type(value).__name__}. This is likely a backend error."
52
+ )
53
+ return value
54
+
55
+
56
+ def build_aip_page(
57
+ data: Dict[str, Any],
58
+ items_key: str,
59
+ item_class: Type[T],
60
+ fetch_page: Callable[[str], Page[T]],
61
+ ) -> Page[T]:
62
+ """Construct Page[T] from an AIP list response with a Pydantic item class."""
63
+ if not isinstance(data, dict) or items_key not in data:
64
+ raise ModusError(
65
+ f"Unexpected list response shape: missing {items_key!r}. "
66
+ f"Got: {type(data).__name__}. This is likely a backend error."
67
+ )
68
+ raw_items: List[Dict[str, Any]] = _require_items_list(data, items_key)
69
+ items = [item_class.model_validate(raw) for raw in raw_items] # type: ignore[attr-defined]
70
+ return Page(
71
+ items=items,
72
+ next_page_token=_next_page_token_from_response(data),
73
+ _fetch_page=fetch_page,
74
+ )
75
+
76
+
77
+ def build_async_aip_page(
78
+ data: Dict[str, Any],
79
+ items_key: str,
80
+ item_class: Type[T],
81
+ fetch_page: Callable[[str], Awaitable[AsyncPage[T]]],
82
+ ) -> AsyncPage[T]:
83
+ """Construct AsyncPage[T] from an AIP list response with a Pydantic item class."""
84
+ if not isinstance(data, dict) or items_key not in data:
85
+ raise ModusError(
86
+ f"Unexpected list response shape: missing {items_key!r}. "
87
+ f"Got: {type(data).__name__}. This is likely a backend error."
88
+ )
89
+ raw_items: List[Dict[str, Any]] = _require_items_list(data, items_key)
90
+ items = [item_class.model_validate(raw) for raw in raw_items] # type: ignore[attr-defined]
91
+ return AsyncPage(
92
+ items=items,
93
+ next_page_token=_next_page_token_from_response(data),
94
+ _fetch_page=fetch_page,
95
+ )
96
+
97
+
98
+ def build_aip_page_mapped(
99
+ data: Dict[str, Any],
100
+ items_key: str,
101
+ map_item: Callable[[Dict[str, Any]], T],
102
+ fetch_page: Callable[[str], Page[T]],
103
+ ) -> Page[T]:
104
+ """Construct Page[T] from an AIP list response using a per-item mapper."""
105
+ if not isinstance(data, dict) or items_key not in data:
106
+ raise ModusError(
107
+ f"Unexpected list response shape: missing {items_key!r}. "
108
+ f"Got: {type(data).__name__}. This is likely a backend error."
109
+ )
110
+ raw_items: List[Dict[str, Any]] = _require_items_list(data, items_key)
111
+ items = [map_item(raw) for raw in raw_items]
112
+ return Page(
113
+ items=items,
114
+ next_page_token=_next_page_token_from_response(data),
115
+ _fetch_page=fetch_page,
116
+ )
117
+
118
+
119
+ def build_async_aip_page_mapped(
120
+ data: Dict[str, Any],
121
+ items_key: str,
122
+ map_item: Callable[[Dict[str, Any]], T],
123
+ fetch_page: Callable[[str], Awaitable[AsyncPage[T]]],
124
+ ) -> AsyncPage[T]:
125
+ """Construct AsyncPage[T] from an AIP list response using a per-item mapper."""
126
+ if not isinstance(data, dict) or items_key not in data:
127
+ raise ModusError(
128
+ f"Unexpected list response shape: missing {items_key!r}. "
129
+ f"Got: {type(data).__name__}. This is likely a backend error."
130
+ )
131
+ raw_items: List[Dict[str, Any]] = _require_items_list(data, items_key)
132
+ items = [map_item(raw) for raw in raw_items]
133
+ return AsyncPage(
134
+ items=items,
135
+ next_page_token=_next_page_token_from_response(data),
136
+ _fetch_page=fetch_page,
137
+ )
138
+
139
+
140
+ def build_aip_page_raw(
141
+ data: Dict[str, Any],
142
+ items_key: str,
143
+ fetch_page: Callable[[str], Page[Dict[str, Any]]],
144
+ ) -> Page[Dict[str, Any]]:
145
+ """Construct Page[dict] from an AIP list response (e.g. context item values)."""
146
+ if not isinstance(data, dict) or items_key not in data:
147
+ raise ModusError(
148
+ f"Unexpected list response shape: missing {items_key!r}. "
149
+ f"Got: {type(data).__name__}. This is likely a backend error."
150
+ )
151
+ items: List[Dict[str, Any]] = [dict(x) for x in _require_items_list(data, items_key)]
152
+ return Page(
153
+ items=items,
154
+ next_page_token=_next_page_token_from_response(data),
155
+ _fetch_page=fetch_page,
156
+ )
157
+
158
+
159
+ def build_async_aip_page_raw(
160
+ data: Dict[str, Any],
161
+ items_key: str,
162
+ fetch_page: Callable[[str], Awaitable[AsyncPage[Dict[str, Any]]]],
163
+ ) -> AsyncPage[Dict[str, Any]]:
164
+ """Construct AsyncPage[dict] from an AIP list response."""
165
+ if not isinstance(data, dict) or items_key not in data:
166
+ raise ModusError(
167
+ f"Unexpected list response shape: missing {items_key!r}. "
168
+ f"Got: {type(data).__name__}. This is likely a backend error."
169
+ )
170
+ items: List[Dict[str, Any]] = [dict(x) for x in _require_items_list(data, items_key)]
171
+ return AsyncPage(
172
+ items=items,
173
+ next_page_token=_next_page_token_from_response(data),
174
+ _fetch_page=fetch_page,
175
+ )
modus/_query.py ADDED
@@ -0,0 +1,38 @@
1
+ """Small helpers for building request URLs."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional
5
+ from urllib.parse import quote
6
+
7
+
8
+ def _validate_update_mask(update_mask: str) -> None:
9
+ if not update_mask.strip():
10
+ raise ValueError(
11
+ "update_mask must be a non-empty comma-separated list of field names"
12
+ )
13
+
14
+
15
+ def update_mask_query(update_mask: Optional[str]) -> Optional[dict[str, str]]:
16
+ """Build an ``updateMask`` query dict for PATCH via ``invoke_operation``."""
17
+ if update_mask is None:
18
+ return None
19
+ _validate_update_mask(update_mask)
20
+ return {"updateMask": update_mask}
21
+
22
+
23
+ def with_update_mask(path: str, update_mask: Optional[str]) -> str:
24
+ """Append an ``updateMask`` query parameter to ``path`` when provided.
25
+
26
+ The API uses ``updateMask`` (a comma-separated list of field names) on PATCH
27
+ requests to explicitly clear fields. When ``update_mask`` is ``None`` the path
28
+ is returned unchanged.
29
+
30
+ Raises:
31
+ ValueError: If ``update_mask`` is given but empty or whitespace-only — an
32
+ empty ``updateMask=`` query value has ambiguous PATCH semantics.
33
+ """
34
+ if update_mask is None:
35
+ return path
36
+ _validate_update_mask(update_mask)
37
+ sep = "&" if "?" in path else "?"
38
+ return f"{path}{sep}updateMask={quote(update_mask, safe='')}"
modus/_retry.py ADDED
@@ -0,0 +1,123 @@
1
+ """
2
+ Retry logic for transient errors.
3
+
4
+ RateLimitError, InternalServerError, and APIConnectionError are retried.
5
+ Client errors (4xx except 429) are not retried — they represent caller mistakes.
6
+
7
+ Backoff strategy:
8
+ - attempt 0 -> no sleep (immediate retry)
9
+ - attempt 1 -> ~1s + jitter
10
+ - attempt 2 -> ~2s + jitter
11
+ - If Retry-After header is present on 429, use that value instead.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import random
18
+ import time
19
+ from typing import Awaitable, Callable, TypeVar
20
+
21
+ from ._exceptions import APIConnectionError, InternalServerError, NotFoundError, RateLimitError
22
+
23
+ T = TypeVar("T")
24
+
25
+ _RETRYABLE_ERRORS = (RateLimitError, InternalServerError, APIConnectionError)
26
+ _DEPLOY_NOT_FOUND_BACKOFF_SECONDS = 0.5
27
+ _MAX_DEPLOY_NOT_FOUND_RETRIES = 1
28
+
29
+
30
+ def _backoff_seconds(attempt: int, exc: Exception) -> float:
31
+ """Calculate how long to sleep before the next retry attempt."""
32
+ if isinstance(exc, RateLimitError) and exc.retry_after is not None:
33
+ return max(0.0, float(exc.retry_after))
34
+ if attempt == 0:
35
+ return 0.0
36
+ # Exponential backoff: 1s, 2s, ... with ±0.3s jitter
37
+ return max(0.5, (2 ** (attempt - 1)) + (random.random() * 0.6 - 0.3))
38
+
39
+
40
+ def with_retry(
41
+ func: Callable[[], T],
42
+ max_retries: int,
43
+ *,
44
+ retry_not_found_on_deploy: bool = False,
45
+ ) -> T:
46
+ """
47
+ Call func, retrying on RateLimitError, InternalServerError, and APIConnectionError.
48
+
49
+ Args:
50
+ func: Zero-argument callable to invoke.
51
+ max_retries: Maximum number of additional attempts after the first.
52
+ 0 means no retries (single attempt total).
53
+ retry_not_found_on_deploy:
54
+ When True, retry HTTP 404 once with a short fixed delay.
55
+ This deploy-specific retry does not consume max_retries.
56
+
57
+ Returns:
58
+ The return value of func on success.
59
+
60
+ Raises:
61
+ The last exception if all attempts are exhausted.
62
+ Any non-retryable exception immediately (no retry).
63
+ """
64
+ deploy_not_found_retries = 0
65
+ attempt = 0
66
+ while True:
67
+ try:
68
+ return func()
69
+ except NotFoundError:
70
+ if (
71
+ retry_not_found_on_deploy
72
+ and deploy_not_found_retries < _MAX_DEPLOY_NOT_FOUND_RETRIES
73
+ ):
74
+ deploy_not_found_retries += 1
75
+ time.sleep(_DEPLOY_NOT_FOUND_BACKOFF_SECONDS)
76
+ continue
77
+ raise
78
+ except _RETRYABLE_ERRORS as exc:
79
+ if attempt >= max_retries:
80
+ raise
81
+ time.sleep(_backoff_seconds(attempt, exc))
82
+ attempt += 1
83
+
84
+
85
+ async def async_with_retry(
86
+ func: Callable[[], Awaitable[T]],
87
+ max_retries: int,
88
+ *,
89
+ retry_not_found_on_deploy: bool = False,
90
+ ) -> T:
91
+ """
92
+ Async version of with_retry. Uses asyncio.sleep so the event loop is
93
+ not blocked during backoff waits.
94
+
95
+ Args:
96
+ func: Zero-argument async callable (coroutine function) to invoke.
97
+ max_retries: Maximum number of additional attempts after the first.
98
+ retry_not_found_on_deploy:
99
+ When True, retry HTTP 404 once with a short fixed delay.
100
+ This deploy-specific retry does not consume max_retries.
101
+
102
+ Returns:
103
+ The return value of func on success.
104
+ """
105
+ deploy_not_found_retries = 0
106
+ attempt = 0
107
+ while True:
108
+ try:
109
+ return await func()
110
+ except NotFoundError:
111
+ if (
112
+ retry_not_found_on_deploy
113
+ and deploy_not_found_retries < _MAX_DEPLOY_NOT_FOUND_RETRIES
114
+ ):
115
+ deploy_not_found_retries += 1
116
+ await asyncio.sleep(_DEPLOY_NOT_FOUND_BACKOFF_SECONDS)
117
+ continue
118
+ raise
119
+ except _RETRYABLE_ERRORS as exc:
120
+ if attempt >= max_retries:
121
+ raise
122
+ await asyncio.sleep(_backoff_seconds(attempt, exc))
123
+ attempt += 1
modus/_streaming.py ADDED
@@ -0,0 +1,115 @@
1
+ """
2
+ SSE stream parser.
3
+
4
+ parse_sse_stream() converts raw SSE lines into typed RunEvent objects.
5
+
6
+ Events and their handling:
7
+ token -> TokenEvent (yielded to text_stream)
8
+ done -> DoneEvent (ends iteration; use ChatResult from chat())
9
+ error -> ErrorEvent (raised as ModusError)
10
+ cancelled -> CancelledEvent (raised as RunCancelledError)
11
+ stream_timeout -> StreamTimeoutEvent (raised as StreamTimeoutError)
12
+ all others -> silently ignored
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from typing import Any, Dict, Iterator, Optional
19
+
20
+ from .types.runs import (
21
+ AssistantContentResetEvent,
22
+ CancelledEvent,
23
+ DoneEvent,
24
+ ErrorEvent,
25
+ RunEvent,
26
+ StreamTimeoutEvent,
27
+ TokenEvent,
28
+ )
29
+
30
+ _SILENTLY_IGNORED = frozenset(
31
+ {
32
+ "heartbeat",
33
+ "ws_chunk",
34
+ "unknown",
35
+ "tool_start",
36
+ "tool_end",
37
+ "tool_chunk",
38
+ "agent_thinking",
39
+ "warning",
40
+ "selected_final_content",
41
+ }
42
+ )
43
+
44
+
45
+ def _parse_line(line: str) -> Optional[Dict[str, Any]]:
46
+ """
47
+ Parse a single SSE line. Returns the parsed JSON payload or None.
48
+ SSE lines that carry data start with "data: ".
49
+ """
50
+ if not line.startswith("data: "):
51
+ return None
52
+ raw = line[6:].strip()
53
+ if not raw or raw == "[DONE]":
54
+ return None
55
+ try:
56
+ return json.loads(raw)
57
+ except json.JSONDecodeError:
58
+ return None
59
+
60
+
61
+ def parse_sse_stream(lines: Iterator[str]) -> Iterator[RunEvent]:
62
+ """
63
+ Convert a raw SSE line iterator into typed RunEvent objects.
64
+
65
+ Silently drops keepalive and unrecognized event types.
66
+ Yields only: TokenEvent, DoneEvent, ErrorEvent, CancelledEvent,
67
+ StreamTimeoutEvent.
68
+ """
69
+ for line in lines:
70
+ data = _parse_line(line)
71
+ if data is None:
72
+ continue
73
+
74
+ event_type = data.get("type", "unknown")
75
+
76
+ if event_type == "token":
77
+ yield TokenEvent(content=data.get("content", ""))
78
+ elif event_type == "done":
79
+ yield DoneEvent(
80
+ run_id=data.get("runId", ""),
81
+ thread_id=data.get("threadId", ""),
82
+ )
83
+ elif event_type == "error":
84
+ msg = (
85
+ data.get("error")
86
+ or data.get("message")
87
+ or data.get("content")
88
+ or "Unknown run error"
89
+ )
90
+ yield ErrorEvent(message=str(msg))
91
+ elif event_type == "cancelled":
92
+ yield CancelledEvent()
93
+ elif event_type == "stream_timeout":
94
+ yield StreamTimeoutEvent()
95
+ elif event_type == "assistant_content_reset":
96
+ content = data.get("content")
97
+ if not isinstance(content, str):
98
+ continue
99
+ yield AssistantContentResetEvent(
100
+ content=content,
101
+ visible_content=(
102
+ str(data["visibleContent"])
103
+ if isinstance(data.get("visibleContent"), str)
104
+ else None
105
+ ),
106
+ attempt=(int(data["attempt"]) if isinstance(data.get("attempt"), int) else 1),
107
+ reason=(
108
+ str(data["reason"])
109
+ if isinstance(data.get("reason"), str)
110
+ else "provider_stream_failed"
111
+ ),
112
+ )
113
+ elif event_type in _SILENTLY_IGNORED:
114
+ continue
115
+ # Truly unknown types: silently ignored for forward compatibility
modus/_validation.py ADDED
@@ -0,0 +1,61 @@
1
+ """Tiny client-side validators shared across resources.
2
+
3
+ These exist so we fail fast on obvious bad inputs (empty IDs, negative limits)
4
+ before making a network call. They raise ``ValueError`` — caller-visible bugs,
5
+ not server-side conditions.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import Optional, Union
10
+
11
+ # API max page size for context list and values surfaces.
12
+ DEFAULT_MAX_PAGE_SIZE = 200
13
+
14
+
15
+ def validate_page_size(page_size: int, *, max_page_size: Optional[int] = None) -> None:
16
+ """Reject page_size values that the backend would reject anyway, locally."""
17
+ if not isinstance(page_size, int) or isinstance(page_size, bool) or page_size < 1:
18
+ raise ValueError(f"page_size must be a positive int, got {page_size!r}")
19
+ if max_page_size is not None and page_size > max_page_size:
20
+ raise ValueError(
21
+ f"page_size must be <= {max_page_size}, got {page_size!r}"
22
+ )
23
+
24
+
25
+ def validate_id(resource_id: Union[int, str], *, name: str = "id") -> None:
26
+ """Reject empty/whitespace-only string IDs that would produce ambiguous URLs."""
27
+ if isinstance(resource_id, str) and not resource_id.strip():
28
+ raise ValueError(f"{name} must be a non-empty string or int")
29
+
30
+
31
+ # Canonical chat model ids from SkillChatRequestDto (keep in sync with the API spec).
32
+ ALLOWED_CHAT_MODELS = frozenset({
33
+ "claude-sonnet-5",
34
+ "claude-sonnet-4.6",
35
+ "claude-opus-4.8",
36
+ "gpt-5.2",
37
+ "gpt-5.5",
38
+ "gpt-5-mini",
39
+ "gpt-oss-120b",
40
+ "gpt-oss-20b",
41
+ "qwen3-235b-a22b-2507",
42
+ "qwen3-coder",
43
+ "qwen3-32b",
44
+ "deepseek-chat-v3.1",
45
+ "minimax-m2.7",
46
+ "llama-4-maverick",
47
+ "llama-4-scout",
48
+ "llama-3.3-70b-instruct",
49
+ "gemini-3.1-pro-preview",
50
+ "gemini-3-flash-preview",
51
+ "grok-4.3-fast",
52
+ "grok-4.3",
53
+ })
54
+
55
+
56
+ def validate_chat_model(model: str) -> None:
57
+ if model not in ALLOWED_CHAT_MODELS:
58
+ supported = ", ".join(sorted(ALLOWED_CHAT_MODELS))
59
+ raise ValueError(
60
+ f"Unsupported model {model!r}. Choose one of: {supported}."
61
+ )
modus/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.2.0'
22
+ __version_tuple__ = version_tuple = (0, 2, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,74 @@
1
+ """
2
+ modus.management — configuration surface for the Modus SDK.
3
+
4
+ from modus.management import ModusManagement
5
+ mgmt = ModusManagement(api_key="modus_...")
6
+ mgmt.skills.create(name="Analyst", model="gpt-4o")
7
+
8
+ Errors are the shared modus.* hierarchy (modus.NotFoundError, etc.).
9
+ """
10
+
11
+ from ._client import AsyncModusManagement, ModusManagement
12
+ from .types import (
13
+ Agent,
14
+ AgentSelection,
15
+ AgentSelectionEntry,
16
+ AgentStatus,
17
+ AgentTrigger,
18
+ AgentTriggerValue,
19
+ AgentType,
20
+ AsyncPage,
21
+ ContextItem,
22
+ ContextItemDeletion,
23
+ ContextItemUpdate,
24
+ ContextType,
25
+ Conversation,
26
+ ConversationListItem,
27
+ CreatedContextItem,
28
+ JsonObject,
29
+ ManualTrigger,
30
+ Memory,
31
+ MemorySearchRequest,
32
+ MemoryUpdate,
33
+ Message,
34
+ Page,
35
+ ScheduleTrigger,
36
+ Skill,
37
+ SkillStatus,
38
+ Toolset,
39
+ TriggerSchedule,
40
+ WorkflowGraph,
41
+ )
42
+
43
+ __all__ = [
44
+ "Agent",
45
+ "AgentSelection",
46
+ "AgentSelectionEntry",
47
+ "AgentStatus",
48
+ "AgentTrigger",
49
+ "AgentTriggerValue",
50
+ "AgentType",
51
+ "AsyncModusManagement",
52
+ "AsyncPage",
53
+ "ContextItem",
54
+ "ContextItemDeletion",
55
+ "ContextItemUpdate",
56
+ "ContextType",
57
+ "Conversation",
58
+ "ConversationListItem",
59
+ "CreatedContextItem",
60
+ "JsonObject",
61
+ "ManualTrigger",
62
+ "Memory",
63
+ "MemorySearchRequest",
64
+ "MemoryUpdate",
65
+ "Message",
66
+ "ModusManagement",
67
+ "Page",
68
+ "ScheduleTrigger",
69
+ "Skill",
70
+ "SkillStatus",
71
+ "Toolset",
72
+ "TriggerSchedule",
73
+ "WorkflowGraph",
74
+ ]