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/__init__.py ADDED
@@ -0,0 +1,144 @@
1
+ """
2
+ Modus Python SDK — consumption client.
3
+
4
+ Quick start:
5
+ import modus
6
+
7
+ client = modus.Modus(api_key="modus_xxx")
8
+
9
+ for skill in client.skills.list():
10
+ print(skill.name, skill.status)
11
+
12
+ for item in client.context.items.list():
13
+ print(modus.context_item_label(item))
14
+
15
+ To create or configure skills, use modus.management.ModusManagement.
16
+ """
17
+
18
+ from importlib.metadata import PackageNotFoundError
19
+ from importlib.metadata import version as _pkg_version
20
+
21
+ from ._client import AsyncModus, Modus
22
+ from ._exceptions import (
23
+ APIConnectionError,
24
+ AuthenticationError,
25
+ ConflictError,
26
+ InternalServerError,
27
+ ModusError,
28
+ NotFoundError,
29
+ PermissionDeniedError,
30
+ RateLimitError,
31
+ RunCancelledError,
32
+ StreamTimeoutError,
33
+ UnprocessableError,
34
+ )
35
+ from .types import (
36
+ Agent,
37
+ AgentRun,
38
+ AgentRunCreateRequest,
39
+ AgentRunListItem,
40
+ AgentStatus,
41
+ AgentType,
42
+ AsyncPage,
43
+ CancelRunRequest,
44
+ ChatResult,
45
+ Connection,
46
+ ContextItem,
47
+ ContextType,
48
+ Conversation,
49
+ ConversationListItem,
50
+ EditQueuedRunRequest,
51
+ InterruptRunRequest,
52
+ Message,
53
+ ModusRunCreateRequest,
54
+ Page,
55
+ ResumeRunRequest,
56
+ Skill,
57
+ SkillContextComposition,
58
+ SkillRunCreateRequest,
59
+ SkillStatus,
60
+ SuggestionEventSource,
61
+ SuggestionEventType,
62
+ SuggestionQuestion,
63
+ WorkflowActionRequest,
64
+ context_item_label,
65
+ is_verified,
66
+ link_title,
67
+ link_url,
68
+ note_body,
69
+ note_title,
70
+ saved_query_name,
71
+ saved_query_sql,
72
+ )
73
+ from .types.runs import (
74
+ AssistantContentResetEvent,
75
+ CancelledEvent,
76
+ DoneEvent,
77
+ ErrorEvent,
78
+ StreamTimeoutEvent,
79
+ TokenEvent,
80
+ )
81
+
82
+ try:
83
+ __version__ = _pkg_version("modus-sdk")
84
+ except PackageNotFoundError:
85
+ __version__ = "0.0.0.dev0"
86
+
87
+ __all__ = [
88
+ "APIConnectionError",
89
+ "AssistantContentResetEvent",
90
+ "Agent",
91
+ "AgentRun",
92
+ "AgentRunCreateRequest",
93
+ "AgentRunListItem",
94
+ "AgentStatus",
95
+ "AgentType",
96
+ "AsyncModus",
97
+ "AsyncPage",
98
+ "AuthenticationError",
99
+ "CancelledEvent",
100
+ "ChatResult",
101
+ "ConflictError",
102
+ "Connection",
103
+ "CancelRunRequest",
104
+ "ContextItem",
105
+ "ContextType",
106
+ "context_item_label",
107
+ "is_verified",
108
+ "link_title",
109
+ "link_url",
110
+ "note_body",
111
+ "note_title",
112
+ "saved_query_name",
113
+ "saved_query_sql",
114
+ "Conversation",
115
+ "ConversationListItem",
116
+ "DoneEvent",
117
+ "EditQueuedRunRequest",
118
+ "ErrorEvent",
119
+ "InternalServerError",
120
+ "InterruptRunRequest",
121
+ "Message",
122
+ "ModusRunCreateRequest",
123
+ "Modus",
124
+ "ModusError",
125
+ "NotFoundError",
126
+ "Page",
127
+ "PermissionDeniedError",
128
+ "RateLimitError",
129
+ "ResumeRunRequest",
130
+ "RunCancelledError",
131
+ "Skill",
132
+ "SkillContextComposition",
133
+ "SkillRunCreateRequest",
134
+ "SkillStatus",
135
+ "SuggestionEventSource",
136
+ "SuggestionEventType",
137
+ "SuggestionQuestion",
138
+ "StreamTimeoutError",
139
+ "StreamTimeoutEvent",
140
+ "TokenEvent",
141
+ "UnprocessableError",
142
+ "WorkflowActionRequest",
143
+ "__version__",
144
+ ]
modus/_auth.py ADDED
@@ -0,0 +1,111 @@
1
+ """
2
+ API key resolution and auth header generation.
3
+
4
+ The SDK accepts an API key via:
5
+ 1. Explicit argument: Modus(api_key="modus_xxx")
6
+ 2. Environment variable: MODUS_API_KEY=modus_xxx
7
+
8
+ The SDK validates the key format at initialization time so users get a
9
+ clear error immediately rather than a cryptic 401 on the first request.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ from typing import Optional, Union
16
+
17
+ import httpx
18
+
19
+ from ._exceptions import AuthenticationError
20
+
21
+ _ENV_VAR = "MODUS_API_KEY"
22
+ _BASE_URL_ENV_VAR = "MODUS_BASE_URL"
23
+ _AGENT_HOST_ENV_VAR = "MODUS_AGENT_HOST"
24
+ _TIMEOUT_ENV_VAR = "MODUS_TIMEOUT"
25
+ _MAX_RETRIES_ENV_VAR = "MODUS_MAX_RETRIES"
26
+ _KEY_PREFIX = "modus_"
27
+
28
+
29
+ def resolve_api_key(api_key: Optional[str]) -> str:
30
+ """
31
+ Resolve the API key from an explicit argument or the environment.
32
+ Raises AuthenticationError if no key is found or the format is invalid.
33
+ """
34
+ key = api_key or os.environ.get(_ENV_VAR)
35
+
36
+ if not key:
37
+ raise AuthenticationError(
38
+ f"No API key provided. "
39
+ f"Pass api_key= or set the {_ENV_VAR} environment variable. "
40
+ f"Create a token at app.modus.com → Settings → API Tokens."
41
+ )
42
+
43
+ if not key.startswith(_KEY_PREFIX):
44
+ raise AuthenticationError(
45
+ f"Invalid API key format. Modus API keys start with '{_KEY_PREFIX}'. "
46
+ f"Create a token at app.modus.com → Settings → API Tokens."
47
+ )
48
+
49
+ return key
50
+
51
+
52
+ def resolve_base_url(base_url: Optional[str]) -> Optional[str]:
53
+ """
54
+ Resolve the base URL from an explicit constructor argument, or from the
55
+ ``MODUS_BASE_URL`` environment variable when set. Returns ``None`` if
56
+ neither is set (caller uses the production default).
57
+ """
58
+ return base_url or os.environ.get(_BASE_URL_ENV_VAR) or None
59
+
60
+
61
+ def resolve_agent_host(agent_host: Optional[str]) -> Optional[str]:
62
+ """
63
+ Resolve the agent host from an explicit constructor argument, or from the
64
+ ``MODUS_AGENT_HOST`` environment variable when set. Returns ``None`` if
65
+ neither is set (streaming falls back to ``base_url``).
66
+ """
67
+ return agent_host or os.environ.get(_AGENT_HOST_ENV_VAR) or None
68
+
69
+
70
+ def resolve_timeout(explicit: Union[float, httpx.Timeout, None]) -> Union[float, httpx.Timeout, None]:
71
+ """
72
+ Resolve timeout from an explicit argument or the MODUS_TIMEOUT env var.
73
+ Returns None if neither is set (caller will use the default).
74
+
75
+ If MODUS_TIMEOUT is set but invalid, returns None (falls back to default).
76
+ """
77
+ if explicit is not None:
78
+ return explicit
79
+ raw = os.environ.get(_TIMEOUT_ENV_VAR)
80
+ if raw is not None:
81
+ try:
82
+ return float(raw)
83
+ except ValueError:
84
+ return None
85
+ return None
86
+
87
+
88
+ def resolve_max_retries(explicit: Optional[int]) -> Optional[int]:
89
+ """
90
+ Resolve max_retries from an explicit argument or the MODUS_MAX_RETRIES env var.
91
+ Returns None if neither is set (caller will use the default).
92
+
93
+ If MODUS_MAX_RETRIES is set but invalid, returns None (falls back to default).
94
+ """
95
+ if explicit is not None:
96
+ if explicit < 0:
97
+ raise ValueError(f"max_retries must be >= 0, got {explicit}")
98
+ return explicit
99
+ raw = os.environ.get(_MAX_RETRIES_ENV_VAR)
100
+ if raw is not None:
101
+ try:
102
+ value = int(raw)
103
+ return value if value >= 0 else None
104
+ except ValueError:
105
+ return None
106
+ return None
107
+
108
+
109
+ def auth_headers(api_key: str) -> dict[str, str]:
110
+ """Return the Authorization header dict for a given API key."""
111
+ return {"Authorization": f"Bearer {api_key}"}
modus/_chat.py ADDED
@@ -0,0 +1,411 @@
1
+ """Shared chat path building and SSE stream session helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, AsyncIterator, Dict, Iterator, List, Literal, Optional, Union
6
+ from urllib.parse import quote
7
+
8
+ from ._exceptions import ModusError, RunCancelledError, StreamTimeoutError
9
+ from ._generated.models import SkillChatRequestDto, SkillChatResponseDto
10
+ from ._http import AsyncHttpClient, HttpClient
11
+ from ._openapi_invoke import format_operation_path
12
+ from ._streaming import parse_sse_stream
13
+ from ._validation import validate_chat_model, validate_id
14
+ from .types.chat import ChatResult
15
+ from .types.runs import (
16
+ AssistantContentResetEvent,
17
+ CancelledEvent,
18
+ DoneEvent,
19
+ ErrorEvent,
20
+ RunEvent,
21
+ StreamTimeoutEvent,
22
+ TokenEvent,
23
+ )
24
+
25
+ RunVersion = Union[Literal["published", "draft"], str]
26
+
27
+
28
+ def _effective_thread_id(thread_id: Optional[str]) -> Optional[str]:
29
+ """Treat None and blank strings as "start a new thread"."""
30
+ if thread_id is None:
31
+ return None
32
+ if isinstance(thread_id, str) and not thread_id.strip():
33
+ return None
34
+ return thread_id
35
+
36
+
37
+ def _chat_path(
38
+ resource: str,
39
+ resource_id: Union[int, str],
40
+ *,
41
+ thread_id: Optional[str],
42
+ stream: bool,
43
+ ) -> str:
44
+ safe_id = quote(str(resource_id), safe="")
45
+ suffix = "/stream" if stream else ""
46
+ tid = _effective_thread_id(thread_id)
47
+ if tid is not None:
48
+ safe_tid = quote(str(tid), safe="")
49
+ return f"/api/v1/{resource}/{safe_id}/conversations/{safe_tid}/chat{suffix}"
50
+ return f"/api/v1/{resource}/{safe_id}/chat{suffix}"
51
+
52
+
53
+ def _modus_chat_path(*, thread_id: Optional[str], stream: bool) -> str:
54
+ suffix = "/stream" if stream else ""
55
+ if thread_id is not None:
56
+ validate_id(thread_id, name="thread_id")
57
+ tid = _effective_thread_id(thread_id)
58
+ if tid is not None:
59
+ safe_tid = quote(str(tid), safe="")
60
+ return f"/api/v1/modus/conversations/{safe_tid}/chat{suffix}"
61
+ return f"/api/v1/modus/chat{suffix}"
62
+
63
+
64
+ def _chat_body(message: str, model: str) -> Dict[str, Any]:
65
+ return SkillChatRequestDto(message=message, model=model).model_dump( # type: ignore[arg-type]
66
+ by_alias=True,
67
+ exclude_none=True,
68
+ )
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Agent-host runs paths (streaming only)
73
+ # ---------------------------------------------------------------------------
74
+
75
+
76
+ def _agent_host_modus_runs_path() -> str:
77
+ """POST {agent_host}/agent/v1/modus/runs — SSE stream."""
78
+ return "/agent/v1/modus/runs"
79
+
80
+
81
+ def _agent_runs_body(
82
+ message: str,
83
+ model: str,
84
+ *,
85
+ session_id: Optional[str] = None,
86
+ version: Optional[RunVersion] = None,
87
+ ) -> Dict[str, Any]:
88
+ """Build the request body for agent-host runs (streaming) requests.
89
+
90
+ Args:
91
+ message: The user message.
92
+ model: Model id, e.g. ``"claude-sonnet-5"``.
93
+ session_id: Existing conversation/session ID (``thread_id`` on the SDK surface).
94
+ version: Run a specific variation — ``"published"`` or ``"draft"``.
95
+ """
96
+ body: Dict[str, Any] = {
97
+ "message": message,
98
+ "config": {"model": model},
99
+ }
100
+ if session_id is not None:
101
+ body["sessionId"] = session_id
102
+ if version is not None:
103
+ body["version"] = version
104
+ return body
105
+
106
+
107
+ def _raise_for_event(event: RunEvent) -> RunEvent:
108
+ if isinstance(event, ErrorEvent):
109
+ raise ModusError(event.message)
110
+ if isinstance(event, CancelledEvent):
111
+ raise RunCancelledError()
112
+ if isinstance(event, StreamTimeoutEvent):
113
+ raise StreamTimeoutError()
114
+ return event
115
+
116
+
117
+ class ChatStream:
118
+ """Sync context manager for SSE chat streams."""
119
+
120
+ def __init__(
121
+ self,
122
+ http: HttpClient,
123
+ path: str,
124
+ body: Dict[str, Any],
125
+ ) -> None:
126
+ self._http = http
127
+ self._path = path
128
+ self._body = body
129
+ self._final: Optional[ChatResult] = None
130
+ self._text_parts: List[str] = []
131
+
132
+ def __enter__(self) -> "ChatStream":
133
+ return self
134
+
135
+ def __exit__(self, *args: object) -> None:
136
+ return None
137
+
138
+ @property
139
+ def text_stream(self) -> Iterator[str]:
140
+ for event in self._event_stream_impl(supports_reset=False):
141
+ if isinstance(event, TokenEvent):
142
+ yield event.content
143
+
144
+ @property
145
+ def event_stream(self) -> Iterator[RunEvent]:
146
+ return self._event_stream_impl(supports_reset=True)
147
+
148
+ def _event_stream_impl(self, *, supports_reset: bool) -> Iterator[RunEvent]:
149
+ body = dict(self._body)
150
+ if supports_reset:
151
+ body["streamProtocolVersion"] = 2
152
+ for line in self._http.stream_post(self._path, json=body):
153
+ for event in parse_sse_stream(iter([line])):
154
+ event = _raise_for_event(event)
155
+ if isinstance(event, TokenEvent):
156
+ self._text_parts.append(event.content)
157
+ if isinstance(event, AssistantContentResetEvent):
158
+ self._text_parts[:] = [event.content]
159
+ if isinstance(event, DoneEvent):
160
+ content = "".join(self._text_parts)
161
+ self._final = SkillChatResponseDto(
162
+ content=content,
163
+ thread_id=event.thread_id,
164
+ run_id=event.run_id,
165
+ )
166
+ yield event
167
+ return
168
+ yield event
169
+
170
+ def get_final_result(self) -> ChatResult:
171
+ if self._final is None:
172
+ raise ModusError(
173
+ "Stream ended without a done event; call get_final_result() only after "
174
+ "consuming text_stream or event_stream."
175
+ )
176
+ return self._final
177
+
178
+
179
+ class AsyncChatStream:
180
+ """Async context manager for SSE chat streams."""
181
+
182
+ def __init__(
183
+ self,
184
+ http: AsyncHttpClient,
185
+ path: str,
186
+ body: Dict[str, Any],
187
+ ) -> None:
188
+ self._http = http
189
+ self._path = path
190
+ self._body = body
191
+ self._final: Optional[ChatResult] = None
192
+ self._text_parts: List[str] = []
193
+
194
+ async def __aenter__(self) -> "AsyncChatStream":
195
+ return self
196
+
197
+ async def __aexit__(self, *args: object) -> None:
198
+ return None
199
+
200
+ @property
201
+ def text_stream(self) -> AsyncIterator[str]:
202
+ return self._text_stream_impl()
203
+
204
+ async def _text_stream_impl(self) -> AsyncIterator[str]:
205
+ async for event in self._event_stream_impl(supports_reset=False):
206
+ if isinstance(event, TokenEvent):
207
+ yield event.content
208
+
209
+ @property
210
+ def event_stream(self) -> AsyncIterator[RunEvent]:
211
+ return self._event_stream_impl(supports_reset=True)
212
+
213
+ async def _event_stream_impl(self, *, supports_reset: bool) -> AsyncIterator[RunEvent]:
214
+ body = dict(self._body)
215
+ if supports_reset:
216
+ body["streamProtocolVersion"] = 2
217
+ async for line in self._http.stream_post(self._path, json=body):
218
+ for event in parse_sse_stream(iter([line])):
219
+ event = _raise_for_event(event)
220
+ if isinstance(event, TokenEvent):
221
+ self._text_parts.append(event.content)
222
+ if isinstance(event, AssistantContentResetEvent):
223
+ self._text_parts[:] = [event.content]
224
+ if isinstance(event, DoneEvent):
225
+ content = "".join(self._text_parts)
226
+ self._final = SkillChatResponseDto(
227
+ content=content,
228
+ thread_id=event.thread_id,
229
+ run_id=event.run_id,
230
+ )
231
+ yield event
232
+ return
233
+ yield event
234
+
235
+ def get_final_result(self) -> ChatResult:
236
+ if self._final is None:
237
+ raise ModusError(
238
+ "Stream ended without a done event; call get_final_result() only after "
239
+ "consuming text_stream or event_stream."
240
+ )
241
+ return self._final
242
+
243
+
244
+ def chat_buffered(
245
+ http: HttpClient,
246
+ resource: str,
247
+ resource_id: Union[int, str],
248
+ message: str,
249
+ *,
250
+ model: str,
251
+ thread_id: Optional[str] = None,
252
+ ) -> ChatResult:
253
+ """POST buffered chat (JSON response)."""
254
+ validate_id(resource_id, name=f"{resource}_id")
255
+ validate_chat_model(model)
256
+ path = _chat_path(resource, resource_id, thread_id=thread_id, stream=False)
257
+ data = http.post(path, json=_chat_body(message, model))
258
+ return SkillChatResponseDto.model_validate(data)
259
+
260
+
261
+ async def chat_buffered_async(
262
+ http: AsyncHttpClient,
263
+ resource: str,
264
+ resource_id: Union[int, str],
265
+ message: str,
266
+ *,
267
+ model: str,
268
+ thread_id: Optional[str] = None,
269
+ ) -> ChatResult:
270
+ validate_id(resource_id, name=f"{resource}_id")
271
+ validate_chat_model(model)
272
+ path = _chat_path(resource, resource_id, thread_id=thread_id, stream=False)
273
+ data = await http.post(path, json=_chat_body(message, model))
274
+ return SkillChatResponseDto.model_validate(data)
275
+
276
+
277
+ def modus_chat_buffered(
278
+ http: HttpClient,
279
+ message: str,
280
+ *,
281
+ model: str,
282
+ thread_id: Optional[str] = None,
283
+ ) -> ChatResult:
284
+ validate_chat_model(model)
285
+ path = _modus_chat_path(thread_id=thread_id, stream=False)
286
+ data = http.post(path, json=_chat_body(message, model))
287
+ return SkillChatResponseDto.model_validate(data)
288
+
289
+
290
+ async def modus_chat_buffered_async(
291
+ http: AsyncHttpClient,
292
+ message: str,
293
+ *,
294
+ model: str,
295
+ thread_id: Optional[str] = None,
296
+ ) -> ChatResult:
297
+ validate_chat_model(model)
298
+ path = _modus_chat_path(thread_id=thread_id, stream=False)
299
+ data = await http.post(path, json=_chat_body(message, model))
300
+ return SkillChatResponseDto.model_validate(data)
301
+
302
+
303
+ # ---------------------------------------------------------------------------
304
+ # Agent-host stream sessions (scopes / modus / workflows)
305
+ #
306
+ # Scope and workflow run paths (/agent/v1/scopes|workflows/{id}/runs) are
307
+ # resolved from the generated OPERATIONS registry rather than a hardcoded
308
+ # literal; the Modus run path is a fixed agent-host route.
309
+ # ---------------------------------------------------------------------------
310
+
311
+
312
+ def modus_stream_session(
313
+ agent_http: HttpClient,
314
+ message: str,
315
+ *,
316
+ model: str,
317
+ thread_id: Optional[str] = None,
318
+ ) -> ChatStream:
319
+ """Stream a Modus chat via the agent host's by-reference runs path."""
320
+ validate_chat_model(model)
321
+ session_id = _effective_thread_id(thread_id)
322
+ path = _agent_host_modus_runs_path()
323
+ body = _agent_runs_body(message, model, session_id=session_id)
324
+ return ChatStream(agent_http, path, body)
325
+
326
+
327
+ def modus_stream_session_async(
328
+ agent_http: AsyncHttpClient,
329
+ message: str,
330
+ *,
331
+ model: str,
332
+ thread_id: Optional[str] = None,
333
+ ) -> AsyncChatStream:
334
+ """Async stream a Modus chat via the agent host's by-reference runs path."""
335
+ validate_chat_model(model)
336
+ session_id = _effective_thread_id(thread_id)
337
+ path = _agent_host_modus_runs_path()
338
+ body = _agent_runs_body(message, model, session_id=session_id)
339
+ return AsyncChatStream(agent_http, path, body)
340
+
341
+
342
+ def scope_stream_session(
343
+ agent_http: HttpClient,
344
+ resource_id: Union[int, str],
345
+ message: str,
346
+ *,
347
+ model: str,
348
+ thread_id: Optional[str] = None,
349
+ version: Optional[RunVersion] = None,
350
+ ) -> ChatStream:
351
+ """Stream a scope chat via the agent host's by-reference runs path."""
352
+ validate_id(resource_id, name="scope_id")
353
+ validate_chat_model(model)
354
+ session_id = _effective_thread_id(thread_id)
355
+ path = format_operation_path("ScopeRunsController_create", {"id": resource_id})
356
+ body = _agent_runs_body(message, model, session_id=session_id, version=version)
357
+ return ChatStream(agent_http, path, body)
358
+
359
+
360
+ def scope_stream_session_async(
361
+ agent_http: AsyncHttpClient,
362
+ resource_id: Union[int, str],
363
+ message: str,
364
+ *,
365
+ model: str,
366
+ thread_id: Optional[str] = None,
367
+ version: Optional[RunVersion] = None,
368
+ ) -> AsyncChatStream:
369
+ """Async stream a scope chat via the agent host's by-reference runs path."""
370
+ validate_id(resource_id, name="scope_id")
371
+ validate_chat_model(model)
372
+ session_id = _effective_thread_id(thread_id)
373
+ path = format_operation_path("ScopeRunsController_create", {"id": resource_id})
374
+ body = _agent_runs_body(message, model, session_id=session_id, version=version)
375
+ return AsyncChatStream(agent_http, path, body)
376
+
377
+
378
+ def workflow_stream_session(
379
+ agent_http: HttpClient,
380
+ resource_id: Union[int, str],
381
+ message: str,
382
+ *,
383
+ model: str,
384
+ thread_id: Optional[str] = None,
385
+ version: Optional[RunVersion] = None,
386
+ ) -> ChatStream:
387
+ """Stream a workflow chat via the agent host's by-reference runs path."""
388
+ validate_id(resource_id, name="workflow_id")
389
+ validate_chat_model(model)
390
+ session_id = _effective_thread_id(thread_id)
391
+ path = format_operation_path("WorkflowRunsController_create", {"id": resource_id})
392
+ body = _agent_runs_body(message, model, session_id=session_id, version=version)
393
+ return ChatStream(agent_http, path, body)
394
+
395
+
396
+ def workflow_stream_session_async(
397
+ agent_http: AsyncHttpClient,
398
+ resource_id: Union[int, str],
399
+ message: str,
400
+ *,
401
+ model: str,
402
+ thread_id: Optional[str] = None,
403
+ version: Optional[RunVersion] = None,
404
+ ) -> AsyncChatStream:
405
+ """Async stream a workflow chat via the agent host's by-reference runs path."""
406
+ validate_id(resource_id, name="workflow_id")
407
+ validate_chat_model(model)
408
+ session_id = _effective_thread_id(thread_id)
409
+ path = format_operation_path("WorkflowRunsController_create", {"id": resource_id})
410
+ body = _agent_runs_body(message, model, session_id=session_id, version=version)
411
+ return AsyncChatStream(agent_http, path, body)