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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ModusManagement — synchronous client for managing Modus scopes, workflows, and context.
|
|
3
|
+
AsyncModusManagement — asynchronous client for the same.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Optional, Union
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from modus._auth import resolve_api_key, resolve_base_url, resolve_max_retries, resolve_timeout
|
|
13
|
+
from modus._config import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, ModusConfig
|
|
14
|
+
from modus._http import AsyncHttpClient, HttpClient
|
|
15
|
+
|
|
16
|
+
from .resources import (
|
|
17
|
+
AsyncContextResource,
|
|
18
|
+
AsyncOrganizationResource,
|
|
19
|
+
AsyncScopesResource,
|
|
20
|
+
AsyncUsageResource,
|
|
21
|
+
AsyncUsersResource,
|
|
22
|
+
AsyncWorkflowsResource,
|
|
23
|
+
ContextResource,
|
|
24
|
+
OrganizationResource,
|
|
25
|
+
ScopesResource,
|
|
26
|
+
UsageResource,
|
|
27
|
+
UsersResource,
|
|
28
|
+
WorkflowsResource,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ModusManagement:
|
|
33
|
+
"""
|
|
34
|
+
Synchronous Modus management client.
|
|
35
|
+
|
|
36
|
+
Instantiate once and reuse. Thread-safe for concurrent reads.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
api_key: Your Modus API key (requires scopes:write + workflows:write scopes).
|
|
40
|
+
Falls back to MODUS_API_KEY env var.
|
|
41
|
+
base_url: API origin without a path suffix (default
|
|
42
|
+
https://api.modus.com).
|
|
43
|
+
timeout: Request timeout as seconds or httpx.Timeout. Default is
|
|
44
|
+
5s connect and 300s read.
|
|
45
|
+
max_retries: Retries on 429/5xx (default 2).
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
from modus.management import ModusManagement
|
|
49
|
+
|
|
50
|
+
client = ModusManagement(api_key="modus_xxx")
|
|
51
|
+
|
|
52
|
+
scope = client.scopes.create(
|
|
53
|
+
name="Snowflake Analyst",
|
|
54
|
+
instructions=["Answer data questions using Snowflake."],
|
|
55
|
+
model="gpt-4o",
|
|
56
|
+
)
|
|
57
|
+
client.scopes.deploy(scope.id)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
api_key: Optional[str] = None,
|
|
63
|
+
*,
|
|
64
|
+
base_url: Optional[str] = None,
|
|
65
|
+
timeout: Union[float, httpx.Timeout, None] = None,
|
|
66
|
+
max_retries: Optional[int] = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
resolved_key = resolve_api_key(api_key)
|
|
69
|
+
resolved_url = resolve_base_url(base_url) or DEFAULT_BASE_URL
|
|
70
|
+
_t = resolve_timeout(timeout)
|
|
71
|
+
resolved_timeout = _t if _t is not None else DEFAULT_TIMEOUT
|
|
72
|
+
resolved_retries = resolve_max_retries(max_retries)
|
|
73
|
+
if resolved_retries is None:
|
|
74
|
+
resolved_retries = DEFAULT_MAX_RETRIES
|
|
75
|
+
|
|
76
|
+
self._config = ModusConfig(
|
|
77
|
+
api_key=resolved_key,
|
|
78
|
+
base_url=resolved_url,
|
|
79
|
+
timeout=resolved_timeout,
|
|
80
|
+
max_retries=resolved_retries,
|
|
81
|
+
)
|
|
82
|
+
self._http = HttpClient(self._config)
|
|
83
|
+
|
|
84
|
+
self.scopes: ScopesResource = ScopesResource(self._http, self._config)
|
|
85
|
+
self.workflows: WorkflowsResource = WorkflowsResource(self._http, self._config)
|
|
86
|
+
self.context: ContextResource = ContextResource(self._http, self._config)
|
|
87
|
+
self.usage: UsageResource = UsageResource(self._http, self._config)
|
|
88
|
+
self.users: UsersResource = UsersResource(self._http, self._config)
|
|
89
|
+
self.organization: OrganizationResource = OrganizationResource(self._http, self._config)
|
|
90
|
+
|
|
91
|
+
def close(self) -> None:
|
|
92
|
+
"""Close the underlying HTTP connection pool."""
|
|
93
|
+
self._http.close()
|
|
94
|
+
|
|
95
|
+
def __enter__(self) -> "ModusManagement":
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def __exit__(self, *args: object) -> None:
|
|
99
|
+
self.close()
|
|
100
|
+
|
|
101
|
+
def __repr__(self) -> str:
|
|
102
|
+
return f"ModusManagement(base_url={self._config.base_url!r})"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class AsyncModusManagement:
|
|
106
|
+
"""
|
|
107
|
+
Asynchronous Modus management client.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
api_key: Modus API key with write scopes. Falls back to MODUS_API_KEY.
|
|
111
|
+
base_url: API origin without a path suffix (default
|
|
112
|
+
https://api.modus.com).
|
|
113
|
+
timeout: Request timeout as seconds or httpx.Timeout. Default is
|
|
114
|
+
5s connect and 300s read.
|
|
115
|
+
max_retries: Retries on 429/5xx (default 2).
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
client = AsyncModusManagement(api_key="modus_xxx")
|
|
119
|
+
async def main():
|
|
120
|
+
scope = await client.scopes.create(name="Analyst", model="gpt-4o")
|
|
121
|
+
await client.aclose()
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
api_key: Optional[str] = None,
|
|
127
|
+
*,
|
|
128
|
+
base_url: Optional[str] = None,
|
|
129
|
+
timeout: Union[float, httpx.Timeout, None] = None,
|
|
130
|
+
max_retries: Optional[int] = None,
|
|
131
|
+
) -> None:
|
|
132
|
+
resolved_key = resolve_api_key(api_key)
|
|
133
|
+
resolved_url = resolve_base_url(base_url) or DEFAULT_BASE_URL
|
|
134
|
+
_t = resolve_timeout(timeout)
|
|
135
|
+
resolved_timeout = _t if _t is not None else DEFAULT_TIMEOUT
|
|
136
|
+
resolved_retries = resolve_max_retries(max_retries)
|
|
137
|
+
if resolved_retries is None:
|
|
138
|
+
resolved_retries = DEFAULT_MAX_RETRIES
|
|
139
|
+
|
|
140
|
+
self._config = ModusConfig(
|
|
141
|
+
api_key=resolved_key,
|
|
142
|
+
base_url=resolved_url,
|
|
143
|
+
timeout=resolved_timeout,
|
|
144
|
+
max_retries=resolved_retries,
|
|
145
|
+
)
|
|
146
|
+
self._http = AsyncHttpClient(self._config)
|
|
147
|
+
|
|
148
|
+
self.scopes: AsyncScopesResource = AsyncScopesResource(self._http, self._config)
|
|
149
|
+
self.workflows: AsyncWorkflowsResource = AsyncWorkflowsResource(self._http, self._config)
|
|
150
|
+
self.context: AsyncContextResource = AsyncContextResource(self._http, self._config)
|
|
151
|
+
self.usage: AsyncUsageResource = AsyncUsageResource(self._http, self._config)
|
|
152
|
+
self.users: AsyncUsersResource = AsyncUsersResource(self._http, self._config)
|
|
153
|
+
self.organization: AsyncOrganizationResource = AsyncOrganizationResource(
|
|
154
|
+
self._http, self._config
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
async def aclose(self) -> None:
|
|
158
|
+
"""Close the underlying async HTTP connection pool."""
|
|
159
|
+
await self._http.aclose()
|
|
160
|
+
|
|
161
|
+
async def __aenter__(self) -> "AsyncModusManagement":
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
async def __aexit__(self, *args: object) -> None:
|
|
165
|
+
await self.aclose()
|
|
166
|
+
|
|
167
|
+
def __repr__(self) -> str:
|
|
168
|
+
return f"AsyncModusManagement(base_url={self._config.base_url!r})"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .context.context import AsyncContextResource, ContextResource
|
|
2
|
+
from .organization import AsyncOrganizationResource, OrganizationResource
|
|
3
|
+
from .scopes.scopes import AsyncScopesResource, ScopesResource
|
|
4
|
+
from .usage import AsyncUsageResource, UsageResource
|
|
5
|
+
from .users import AsyncUsersResource, UsersResource
|
|
6
|
+
from .workflows.workflows import AsyncWorkflowsResource, WorkflowsResource
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AsyncContextResource",
|
|
10
|
+
"AsyncOrganizationResource",
|
|
11
|
+
"AsyncScopesResource",
|
|
12
|
+
"AsyncUsageResource",
|
|
13
|
+
"AsyncUsersResource",
|
|
14
|
+
"AsyncWorkflowsResource",
|
|
15
|
+
"ContextResource",
|
|
16
|
+
"OrganizationResource",
|
|
17
|
+
"ScopesResource",
|
|
18
|
+
"UsageResource",
|
|
19
|
+
"UsersResource",
|
|
20
|
+
"WorkflowsResource",
|
|
21
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Resolve accessConfig wire blobs for guardrails= writes (private)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from ..types._guardrails_access import (
|
|
7
|
+
access_config_wire_for_create,
|
|
8
|
+
access_config_wire_for_update,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _access_from_response(raw: Dict[str, Any]) -> Dict[str, Any]:
|
|
13
|
+
value = raw.get("accessConfig") or raw.get("access_config") or {}
|
|
14
|
+
return value if isinstance(value, dict) else {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def access_config_body_for_create(
|
|
18
|
+
guardrails: Optional[List[str]],
|
|
19
|
+
) -> Optional[Dict[str, Any]]:
|
|
20
|
+
if guardrails is None:
|
|
21
|
+
return None
|
|
22
|
+
return access_config_wire_for_create(guardrails)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def access_config_body_for_update(
|
|
26
|
+
guardrails: Optional[List[str]],
|
|
27
|
+
load_resource: Callable[[], Dict[str, Any]],
|
|
28
|
+
) -> Optional[Dict[str, Any]]:
|
|
29
|
+
if guardrails is None:
|
|
30
|
+
return None
|
|
31
|
+
return access_config_wire_for_update(_access_from_response(load_resource()), guardrails)
|
|
File without changes
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Merge helpers for user-created context item content blobs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from modus.types.context import ContextItem
|
|
8
|
+
|
|
9
|
+
from ...types.context import UserFeedback
|
|
10
|
+
from .items import AsyncContextItemsResource, ContextItemsResource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def merge_note_content(existing: dict[str, Any], *, title: str, body: str) -> dict[str, Any]:
|
|
14
|
+
"""Build note ``content`` for ``items.update``, preserving server-owned keys."""
|
|
15
|
+
return {**existing, "title": title, "content": body}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def merge_link_content(
|
|
19
|
+
existing: dict[str, Any],
|
|
20
|
+
*,
|
|
21
|
+
title: str | None = None,
|
|
22
|
+
url: str | None = None,
|
|
23
|
+
) -> dict[str, Any]:
|
|
24
|
+
"""Build link ``content`` for ``items.update``. Does not re-fetch the URL."""
|
|
25
|
+
merged = dict(existing)
|
|
26
|
+
if title is not None:
|
|
27
|
+
merged["title"] = title
|
|
28
|
+
if url is not None:
|
|
29
|
+
merged["url"] = url
|
|
30
|
+
return merged
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def merge_saved_query_content(
|
|
34
|
+
existing: dict[str, Any],
|
|
35
|
+
*,
|
|
36
|
+
name: str,
|
|
37
|
+
query: str,
|
|
38
|
+
connection_id: str | None = None,
|
|
39
|
+
path: list[str] | None = None,
|
|
40
|
+
) -> dict[str, Any]:
|
|
41
|
+
"""Build saved_query ``content`` for ``items.update``."""
|
|
42
|
+
merged: dict[str, Any] = {**existing, "name": name, "query": query}
|
|
43
|
+
if connection_id is not None:
|
|
44
|
+
merged["connectionId"] = connection_id
|
|
45
|
+
if path is not None:
|
|
46
|
+
merged["path"] = path
|
|
47
|
+
return merged
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _require_content_dict(item: ContextItem, *, context_type: str) -> dict[str, Any]:
|
|
51
|
+
if item.context_type != context_type:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"Expected context_type {context_type!r}, got {item.context_type!r}."
|
|
54
|
+
)
|
|
55
|
+
content = item.content
|
|
56
|
+
if not isinstance(content, dict):
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Context item {item.uid!r} has no dict content; cannot merge {context_type} fields."
|
|
59
|
+
)
|
|
60
|
+
return content
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def update_with_merged_content(
|
|
64
|
+
items: ContextItemsResource,
|
|
65
|
+
uid: str,
|
|
66
|
+
*,
|
|
67
|
+
context_type: str,
|
|
68
|
+
merged_content: dict[str, Any],
|
|
69
|
+
existing: ContextItem | None = None,
|
|
70
|
+
description: str | None = None,
|
|
71
|
+
user_feedback: UserFeedback | str | None = None,
|
|
72
|
+
topics: list[str] | None = None,
|
|
73
|
+
) -> ContextItem:
|
|
74
|
+
if existing is not None:
|
|
75
|
+
_require_content_dict(existing, context_type=context_type)
|
|
76
|
+
return items.update(
|
|
77
|
+
uid,
|
|
78
|
+
context_type=context_type,
|
|
79
|
+
content=merged_content,
|
|
80
|
+
description=description,
|
|
81
|
+
user_feedback=user_feedback,
|
|
82
|
+
topics=topics,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def async_update_with_merged_content(
|
|
87
|
+
items: AsyncContextItemsResource,
|
|
88
|
+
uid: str,
|
|
89
|
+
*,
|
|
90
|
+
context_type: str,
|
|
91
|
+
merged_content: dict[str, Any],
|
|
92
|
+
existing: ContextItem | None = None,
|
|
93
|
+
description: str | None = None,
|
|
94
|
+
user_feedback: UserFeedback | str | None = None,
|
|
95
|
+
topics: list[str] | None = None,
|
|
96
|
+
) -> ContextItem:
|
|
97
|
+
if existing is not None:
|
|
98
|
+
_require_content_dict(existing, context_type=context_type)
|
|
99
|
+
return await items.update(
|
|
100
|
+
uid,
|
|
101
|
+
context_type=context_type,
|
|
102
|
+
content=merged_content,
|
|
103
|
+
description=description,
|
|
104
|
+
user_feedback=user_feedback,
|
|
105
|
+
topics=topics,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def resolve_existing_content(
|
|
110
|
+
items: ContextItemsResource,
|
|
111
|
+
uid: str,
|
|
112
|
+
*,
|
|
113
|
+
context_type: str,
|
|
114
|
+
existing: ContextItem | None,
|
|
115
|
+
) -> dict[str, Any]:
|
|
116
|
+
item = existing if existing is not None else items.get(uid)
|
|
117
|
+
return _require_content_dict(item, context_type=context_type)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def async_resolve_existing_content(
|
|
121
|
+
items: AsyncContextItemsResource,
|
|
122
|
+
uid: str,
|
|
123
|
+
*,
|
|
124
|
+
context_type: str,
|
|
125
|
+
existing: ContextItem | None,
|
|
126
|
+
) -> dict[str, Any]:
|
|
127
|
+
item = existing if existing is not None else await items.get(uid)
|
|
128
|
+
return _require_content_dict(item, context_type=context_type)
|