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/__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)
|