meerkat-mobkit 0.4.9__tar.gz
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.
- meerkat_mobkit-0.4.9/PKG-INFO +24 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/__init__.py +171 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/_client.py +521 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/_sse.py +110 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/_transport.py +236 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/agent_builder.py +118 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/builder.py +158 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/config/__init__.py +4 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/config/auth.py +47 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/config/memory.py +30 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/config/session_store.py +45 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/errors.py +32 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/events.py +257 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/helpers.py +147 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/models.py +145 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/py.typed +0 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/runtime.py +822 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit/types.py +549 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit.egg-info/PKG-INFO +24 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit.egg-info/SOURCES.txt +30 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit.egg-info/dependency_links.txt +1 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit.egg-info/requires.txt +3 -0
- meerkat_mobkit-0.4.9/meerkat_mobkit.egg-info/top_level.txt +1 -0
- meerkat_mobkit-0.4.9/pyproject.toml +40 -0
- meerkat_mobkit-0.4.9/setup.cfg +4 -0
- meerkat_mobkit-0.4.9/tests/test_builder.py +80 -0
- meerkat_mobkit-0.4.9/tests/test_callback_tools.py +171 -0
- meerkat_mobkit-0.4.9/tests/test_errors.py +41 -0
- meerkat_mobkit-0.4.9/tests/test_events.py +115 -0
- meerkat_mobkit-0.4.9/tests/test_init.py +76 -0
- meerkat_mobkit-0.4.9/tests/test_models.py +86 -0
- meerkat_mobkit-0.4.9/tests/test_types.py +517 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: meerkat-mobkit
|
|
3
|
+
Version: 0.4.9
|
|
4
|
+
Summary: Python SDK for MobKit — companion orchestration platform for the Meerkat multi-agent runtime
|
|
5
|
+
Author: Luka Crnkovic-Friis
|
|
6
|
+
License: MIT OR Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://docs.rkat.ai
|
|
8
|
+
Project-URL: Repository, https://github.com/lukacf/meerkat-mobkit
|
|
9
|
+
Project-URL: Documentation, https://docs.rkat.ai
|
|
10
|
+
Keywords: agent,llm,ai,meerkat,orchestration,sdk
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""MobKit Python SDK.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from meerkat_mobkit import MobKit, MobKitRuntime, MobKitBuilder
|
|
6
|
+
from meerkat_mobkit import DiscoverySpec, PreSpawnData, SessionQuery
|
|
7
|
+
from meerkat_mobkit import SessionAgentBuilder, SessionBuildOptions
|
|
8
|
+
from meerkat_mobkit.errors import MobKitError, RpcError, NotConnectedError
|
|
9
|
+
from meerkat_mobkit.types import StatusResult, CapabilitiesResult
|
|
10
|
+
from meerkat_mobkit.events import MobEvent, AgentEvent
|
|
11
|
+
|
|
12
|
+
Module authoring helpers are available via::
|
|
13
|
+
|
|
14
|
+
from meerkat_mobkit.helpers import ModuleSpec, define_module, ...
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
# Builder + Runtime
|
|
19
|
+
from .builder import MobKit, MobKitBuilder
|
|
20
|
+
from .runtime import MobKitRuntime, ToolCaller
|
|
21
|
+
|
|
22
|
+
# Data models
|
|
23
|
+
from .models import DiscoverySpec, PreSpawnData, SessionBuildOptions, SessionQuery
|
|
24
|
+
|
|
25
|
+
# Agent builder protocol (public contract — CallbackDispatcher is internal)
|
|
26
|
+
from .agent_builder import SessionAgentBuilder
|
|
27
|
+
|
|
28
|
+
# Errors
|
|
29
|
+
from .errors import (
|
|
30
|
+
CapabilityUnavailableError,
|
|
31
|
+
ContractMismatchError,
|
|
32
|
+
MobKitError,
|
|
33
|
+
NotConnectedError,
|
|
34
|
+
RpcError,
|
|
35
|
+
TransportError,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Typed return models
|
|
39
|
+
from .types import (
|
|
40
|
+
CallToolResult,
|
|
41
|
+
CapabilitiesResult,
|
|
42
|
+
DeliveryHistoryResult,
|
|
43
|
+
DeliveryResult,
|
|
44
|
+
ErrorCategory,
|
|
45
|
+
ErrorEvent,
|
|
46
|
+
EventEnvelope,
|
|
47
|
+
EventQuery,
|
|
48
|
+
GatingAuditEntry,
|
|
49
|
+
GatingDecisionResult,
|
|
50
|
+
GatingEvaluateResult,
|
|
51
|
+
GatingPendingEntry,
|
|
52
|
+
KeepAliveConfig,
|
|
53
|
+
MEMBER_STATE_ACTIVE,
|
|
54
|
+
MEMBER_STATE_RETIRING,
|
|
55
|
+
MemberSnapshot,
|
|
56
|
+
MemoryIndexResult,
|
|
57
|
+
MemoryQueryResult,
|
|
58
|
+
MemoryStoreInfo,
|
|
59
|
+
PersistedEvent,
|
|
60
|
+
ReconcileEdgesReport,
|
|
61
|
+
ReconcileResult,
|
|
62
|
+
RediscoverReport,
|
|
63
|
+
RoutingResolution,
|
|
64
|
+
RuntimeRouteResult,
|
|
65
|
+
SendMessageResult,
|
|
66
|
+
SpawnMemberResult,
|
|
67
|
+
SpawnResult,
|
|
68
|
+
StatusResult,
|
|
69
|
+
SubscribeResult,
|
|
70
|
+
UnifiedAgentEvent,
|
|
71
|
+
UnifiedModuleEvent,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Typed events
|
|
75
|
+
from .events import (
|
|
76
|
+
AgentEvent,
|
|
77
|
+
Event,
|
|
78
|
+
EventStream,
|
|
79
|
+
MobEvent,
|
|
80
|
+
RunCompleted,
|
|
81
|
+
RunFailed,
|
|
82
|
+
RunStarted,
|
|
83
|
+
TextComplete,
|
|
84
|
+
TextDelta,
|
|
85
|
+
ToolCallRequested,
|
|
86
|
+
ToolExecutionCompleted,
|
|
87
|
+
ToolExecutionStarted,
|
|
88
|
+
ToolResultReceived,
|
|
89
|
+
TurnCompleted,
|
|
90
|
+
TurnStarted,
|
|
91
|
+
UnknownEvent,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Config modules (importable as meerkat_mobkit.auth, etc.)
|
|
95
|
+
from .config import auth, memory, session_store
|
|
96
|
+
|
|
97
|
+
__all__ = [
|
|
98
|
+
# Builder + Runtime
|
|
99
|
+
"MobKit",
|
|
100
|
+
"MobKitBuilder",
|
|
101
|
+
"MobKitRuntime",
|
|
102
|
+
# Data models
|
|
103
|
+
"DiscoverySpec",
|
|
104
|
+
"PreSpawnData",
|
|
105
|
+
"SessionBuildOptions",
|
|
106
|
+
"SessionQuery",
|
|
107
|
+
# Agent builder
|
|
108
|
+
"SessionAgentBuilder",
|
|
109
|
+
# Errors
|
|
110
|
+
"MobKitError",
|
|
111
|
+
"TransportError",
|
|
112
|
+
"RpcError",
|
|
113
|
+
"CapabilityUnavailableError",
|
|
114
|
+
"ContractMismatchError",
|
|
115
|
+
"NotConnectedError",
|
|
116
|
+
# Typed return models
|
|
117
|
+
"StatusResult",
|
|
118
|
+
"CapabilitiesResult",
|
|
119
|
+
"ReconcileResult",
|
|
120
|
+
"SpawnResult",
|
|
121
|
+
"SpawnMemberResult",
|
|
122
|
+
"SendMessageResult",
|
|
123
|
+
"SubscribeResult",
|
|
124
|
+
"KeepAliveConfig",
|
|
125
|
+
"EventEnvelope",
|
|
126
|
+
"RoutingResolution",
|
|
127
|
+
"DeliveryResult",
|
|
128
|
+
"DeliveryHistoryResult",
|
|
129
|
+
"MemoryQueryResult",
|
|
130
|
+
"MemoryStoreInfo",
|
|
131
|
+
"MemoryIndexResult",
|
|
132
|
+
"MEMBER_STATE_ACTIVE",
|
|
133
|
+
"MEMBER_STATE_RETIRING",
|
|
134
|
+
"MemberSnapshot",
|
|
135
|
+
"RuntimeRouteResult",
|
|
136
|
+
"GatingEvaluateResult",
|
|
137
|
+
"GatingDecisionResult",
|
|
138
|
+
"GatingAuditEntry",
|
|
139
|
+
"GatingPendingEntry",
|
|
140
|
+
"CallToolResult",
|
|
141
|
+
"ErrorCategory",
|
|
142
|
+
"ErrorEvent",
|
|
143
|
+
"EventQuery",
|
|
144
|
+
"PersistedEvent",
|
|
145
|
+
"UnifiedAgentEvent",
|
|
146
|
+
"UnifiedModuleEvent",
|
|
147
|
+
"ReconcileEdgesReport",
|
|
148
|
+
"RediscoverReport",
|
|
149
|
+
"ToolCaller",
|
|
150
|
+
# Typed events
|
|
151
|
+
"Event",
|
|
152
|
+
"MobEvent",
|
|
153
|
+
"AgentEvent",
|
|
154
|
+
"EventStream",
|
|
155
|
+
"RunStarted",
|
|
156
|
+
"RunCompleted",
|
|
157
|
+
"RunFailed",
|
|
158
|
+
"TurnStarted",
|
|
159
|
+
"TextDelta",
|
|
160
|
+
"TextComplete",
|
|
161
|
+
"ToolCallRequested",
|
|
162
|
+
"ToolResultReceived",
|
|
163
|
+
"TurnCompleted",
|
|
164
|
+
"ToolExecutionStarted",
|
|
165
|
+
"ToolExecutionCompleted",
|
|
166
|
+
"UnknownEvent",
|
|
167
|
+
# Config modules
|
|
168
|
+
"auth",
|
|
169
|
+
"memory",
|
|
170
|
+
"session_store",
|
|
171
|
+
]
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Any, Callable, Literal, Mapping, Protocol, TypedDict, cast
|
|
8
|
+
from urllib import request as urllib_request
|
|
9
|
+
from urllib.error import HTTPError, URLError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JsonRpcRequest(TypedDict):
|
|
13
|
+
jsonrpc: Literal["2.0"]
|
|
14
|
+
id: str
|
|
15
|
+
method: str
|
|
16
|
+
params: dict[str, Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class JsonRpcSuccess(TypedDict):
|
|
20
|
+
jsonrpc: Literal["2.0"]
|
|
21
|
+
id: str
|
|
22
|
+
result: Any
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class JsonRpcErrorBody(TypedDict):
|
|
26
|
+
code: int
|
|
27
|
+
message: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class JsonRpcErrorResponse(TypedDict):
|
|
31
|
+
jsonrpc: Literal["2.0"]
|
|
32
|
+
id: str
|
|
33
|
+
error: JsonRpcErrorBody
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
JsonRpcResponse = JsonRpcSuccess | JsonRpcErrorResponse
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MobkitStatusResult(TypedDict):
|
|
40
|
+
contract_version: str
|
|
41
|
+
running: bool
|
|
42
|
+
loaded_modules: list[str]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MobkitCapabilitiesResult(TypedDict):
|
|
46
|
+
contract_version: str
|
|
47
|
+
methods: list[str]
|
|
48
|
+
loaded_modules: list[str]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MobkitReconcileResult(TypedDict):
|
|
52
|
+
accepted: bool
|
|
53
|
+
reconciled_modules: list[str]
|
|
54
|
+
added: int
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class MobkitSpawnMemberResult(TypedDict):
|
|
58
|
+
accepted: bool
|
|
59
|
+
module_id: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class MobkitSubscribeKeepAlive(TypedDict):
|
|
63
|
+
interval_ms: int
|
|
64
|
+
event: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class MobkitSubscribeEventEnvelope(TypedDict):
|
|
68
|
+
event_id: str
|
|
69
|
+
source: str
|
|
70
|
+
timestamp_ms: int
|
|
71
|
+
event: Any
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class MobkitSubscribeResult(TypedDict):
|
|
75
|
+
scope: Literal["mob", "agent", "interaction"]
|
|
76
|
+
replay_from_event_id: str | None
|
|
77
|
+
keep_alive: MobkitSubscribeKeepAlive
|
|
78
|
+
keep_alive_comment: str
|
|
79
|
+
event_frames: list[str]
|
|
80
|
+
events: list[MobkitSubscribeEventEnvelope]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class MobkitSubscribeParams(TypedDict, total=False):
|
|
84
|
+
scope: Literal["mob", "agent", "interaction"]
|
|
85
|
+
last_event_id: str
|
|
86
|
+
agent_id: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AsyncRpcTransport(Protocol):
|
|
90
|
+
async def __call__(self, request: JsonRpcRequest) -> Any:
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SyncRpcTransport(Protocol):
|
|
95
|
+
def __call__(self, request: JsonRpcRequest) -> Any:
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class MobkitRpcError(RuntimeError):
|
|
100
|
+
def __init__(self, code: int, message: str, request_id: str, method: str):
|
|
101
|
+
super().__init__(message)
|
|
102
|
+
self.code = code
|
|
103
|
+
self.request_id = request_id
|
|
104
|
+
self.method = method
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def create_gateway_sync_transport(gateway_bin: str) -> SyncRpcTransport:
|
|
108
|
+
def transport(request: JsonRpcRequest) -> Any:
|
|
109
|
+
request_json = json.dumps(request)
|
|
110
|
+
proc = subprocess.run(
|
|
111
|
+
[gateway_bin],
|
|
112
|
+
check=False,
|
|
113
|
+
capture_output=True,
|
|
114
|
+
text=True,
|
|
115
|
+
env={**os.environ, "MOBKIT_RPC_REQUEST": request_json},
|
|
116
|
+
)
|
|
117
|
+
if proc.returncode != 0:
|
|
118
|
+
raise RuntimeError(
|
|
119
|
+
f"gateway failed (status={proc.returncode}): {proc.stderr.strip()}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
return json.loads(proc.stdout)
|
|
124
|
+
except json.JSONDecodeError as exc:
|
|
125
|
+
raise ValueError("gateway returned non-JSON response") from exc
|
|
126
|
+
|
|
127
|
+
return transport
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def create_gateway_async_transport(gateway_bin: str) -> AsyncRpcTransport:
|
|
131
|
+
async def transport(request: JsonRpcRequest) -> Any:
|
|
132
|
+
request_json = json.dumps(request)
|
|
133
|
+
proc = await asyncio.create_subprocess_exec(
|
|
134
|
+
gateway_bin,
|
|
135
|
+
env={**os.environ, "MOBKIT_RPC_REQUEST": request_json},
|
|
136
|
+
stdout=asyncio.subprocess.PIPE,
|
|
137
|
+
stderr=asyncio.subprocess.PIPE,
|
|
138
|
+
)
|
|
139
|
+
stdout, stderr = await proc.communicate()
|
|
140
|
+
if proc.returncode != 0:
|
|
141
|
+
stderr_text = stderr.decode("utf-8", errors="replace").strip()
|
|
142
|
+
raise RuntimeError(
|
|
143
|
+
f"gateway failed (status={proc.returncode}): {stderr_text}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
return json.loads(stdout.decode("utf-8"))
|
|
148
|
+
except json.JSONDecodeError as exc:
|
|
149
|
+
raise ValueError("gateway returned non-JSON response") from exc
|
|
150
|
+
|
|
151
|
+
return transport
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def create_http_transport(
|
|
155
|
+
endpoint: str,
|
|
156
|
+
*,
|
|
157
|
+
headers: Mapping[str, str] | None = None,
|
|
158
|
+
timeout_seconds: float = 10.0,
|
|
159
|
+
) -> AsyncRpcTransport:
|
|
160
|
+
base_headers = {"content-type": "application/json", "accept": "application/json"}
|
|
161
|
+
if headers:
|
|
162
|
+
base_headers.update(dict(headers))
|
|
163
|
+
|
|
164
|
+
async def transport(request: JsonRpcRequest) -> Any:
|
|
165
|
+
request_bytes = json.dumps(request).encode("utf-8")
|
|
166
|
+
http_request = urllib_request.Request(
|
|
167
|
+
endpoint,
|
|
168
|
+
data=request_bytes,
|
|
169
|
+
method="POST",
|
|
170
|
+
headers=base_headers,
|
|
171
|
+
)
|
|
172
|
+
try:
|
|
173
|
+
body = await asyncio.to_thread(_read_http_body, http_request, timeout_seconds)
|
|
174
|
+
except HTTPError as exc:
|
|
175
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
176
|
+
raise RuntimeError(
|
|
177
|
+
f"http transport failed (status={exc.code}): {body}"
|
|
178
|
+
) from exc
|
|
179
|
+
except URLError as exc:
|
|
180
|
+
raise RuntimeError(f"http transport failed: {exc.reason}") from exc
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
return json.loads(body)
|
|
184
|
+
except json.JSONDecodeError as exc:
|
|
185
|
+
raise ValueError("http transport returned non-JSON response") from exc
|
|
186
|
+
|
|
187
|
+
return transport
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class MobkitTypedClient:
|
|
191
|
+
def __init__(self, gateway_bin: str):
|
|
192
|
+
self.gateway_bin = gateway_bin
|
|
193
|
+
self._sync_transport = create_gateway_sync_transport(gateway_bin)
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def from_persistent(cls, transport: SyncRpcTransport) -> "MobkitTypedClient":
|
|
197
|
+
instance = cls.__new__(cls)
|
|
198
|
+
instance.gateway_bin = ""
|
|
199
|
+
instance._sync_transport = transport
|
|
200
|
+
return instance
|
|
201
|
+
|
|
202
|
+
def rpc(
|
|
203
|
+
self, request_id: str, method: str, params: Mapping[str, Any] | None = None
|
|
204
|
+
) -> JsonRpcResponse:
|
|
205
|
+
payload = self._sync_transport(_build_request(request_id, method, params))
|
|
206
|
+
return _parse_json_rpc_response(payload, request_id)
|
|
207
|
+
|
|
208
|
+
def status(self, request_id: str = "status") -> MobkitStatusResult:
|
|
209
|
+
return cast(
|
|
210
|
+
MobkitStatusResult,
|
|
211
|
+
_unwrap_typed_result(
|
|
212
|
+
self.rpc(request_id, "mobkit/status", {}),
|
|
213
|
+
request_id,
|
|
214
|
+
"mobkit/status",
|
|
215
|
+
_is_status_result,
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def capabilities(self, request_id: str = "capabilities") -> MobkitCapabilitiesResult:
|
|
220
|
+
return cast(
|
|
221
|
+
MobkitCapabilitiesResult,
|
|
222
|
+
_unwrap_typed_result(
|
|
223
|
+
self.rpc(request_id, "mobkit/capabilities", {}),
|
|
224
|
+
request_id,
|
|
225
|
+
"mobkit/capabilities",
|
|
226
|
+
_is_capabilities_result,
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def reconcile(
|
|
231
|
+
self, modules: list[str], request_id: str = "reconcile"
|
|
232
|
+
) -> MobkitReconcileResult:
|
|
233
|
+
return cast(
|
|
234
|
+
MobkitReconcileResult,
|
|
235
|
+
_unwrap_typed_result(
|
|
236
|
+
self.rpc(request_id, "mobkit/reconcile", {"modules": modules}),
|
|
237
|
+
request_id,
|
|
238
|
+
"mobkit/reconcile",
|
|
239
|
+
_is_reconcile_result,
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def spawn_member(
|
|
244
|
+
self, module_id: str, request_id: str = "spawn_member"
|
|
245
|
+
) -> MobkitSpawnMemberResult:
|
|
246
|
+
return cast(
|
|
247
|
+
MobkitSpawnMemberResult,
|
|
248
|
+
_unwrap_typed_result(
|
|
249
|
+
self.rpc(request_id, "mobkit/spawn_member", {"module_id": module_id}),
|
|
250
|
+
request_id,
|
|
251
|
+
"mobkit/spawn_member",
|
|
252
|
+
_is_spawn_member_result,
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def subscribe_events(
|
|
257
|
+
self,
|
|
258
|
+
params: MobkitSubscribeParams | None = None,
|
|
259
|
+
request_id: str = "events_subscribe",
|
|
260
|
+
) -> MobkitSubscribeResult:
|
|
261
|
+
return cast(
|
|
262
|
+
MobkitSubscribeResult,
|
|
263
|
+
_unwrap_typed_result(
|
|
264
|
+
self.rpc(
|
|
265
|
+
request_id,
|
|
266
|
+
"mobkit/events/subscribe",
|
|
267
|
+
dict(params) if params is not None else {},
|
|
268
|
+
),
|
|
269
|
+
request_id,
|
|
270
|
+
"mobkit/events/subscribe",
|
|
271
|
+
_is_subscribe_result,
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class MobkitAsyncTypedClient:
|
|
277
|
+
def __init__(self, transport: AsyncRpcTransport):
|
|
278
|
+
self._transport = transport
|
|
279
|
+
|
|
280
|
+
@classmethod
|
|
281
|
+
def from_gateway_bin(cls, gateway_bin: str) -> "MobkitAsyncTypedClient":
|
|
282
|
+
return cls(create_gateway_async_transport(gateway_bin))
|
|
283
|
+
|
|
284
|
+
@classmethod
|
|
285
|
+
def from_http(
|
|
286
|
+
cls,
|
|
287
|
+
endpoint: str,
|
|
288
|
+
*,
|
|
289
|
+
headers: Mapping[str, str] | None = None,
|
|
290
|
+
timeout_seconds: float = 10.0,
|
|
291
|
+
) -> "MobkitAsyncTypedClient":
|
|
292
|
+
return cls(
|
|
293
|
+
create_http_transport(
|
|
294
|
+
endpoint,
|
|
295
|
+
headers=headers,
|
|
296
|
+
timeout_seconds=timeout_seconds,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
async def rpc(
|
|
301
|
+
self, request_id: str, method: str, params: Mapping[str, Any] | None = None
|
|
302
|
+
) -> JsonRpcResponse:
|
|
303
|
+
payload = await self._transport(_build_request(request_id, method, params))
|
|
304
|
+
return _parse_json_rpc_response(payload, request_id)
|
|
305
|
+
|
|
306
|
+
async def request(
|
|
307
|
+
self,
|
|
308
|
+
request_id: str,
|
|
309
|
+
method: str,
|
|
310
|
+
params: Mapping[str, Any] | None,
|
|
311
|
+
validator: Callable[[Any], bool],
|
|
312
|
+
) -> Any:
|
|
313
|
+
response = await self.rpc(request_id, method, params)
|
|
314
|
+
return _unwrap_typed_result(response, request_id, method, validator)
|
|
315
|
+
|
|
316
|
+
async def status(self, request_id: str = "status") -> MobkitStatusResult:
|
|
317
|
+
return cast(
|
|
318
|
+
MobkitStatusResult,
|
|
319
|
+
await self.request(request_id, "mobkit/status", {}, _is_status_result),
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
async def capabilities(
|
|
323
|
+
self, request_id: str = "capabilities"
|
|
324
|
+
) -> MobkitCapabilitiesResult:
|
|
325
|
+
return cast(
|
|
326
|
+
MobkitCapabilitiesResult,
|
|
327
|
+
await self.request(
|
|
328
|
+
request_id,
|
|
329
|
+
"mobkit/capabilities",
|
|
330
|
+
{},
|
|
331
|
+
_is_capabilities_result,
|
|
332
|
+
),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
async def reconcile(
|
|
336
|
+
self, modules: list[str], request_id: str = "reconcile"
|
|
337
|
+
) -> MobkitReconcileResult:
|
|
338
|
+
return cast(
|
|
339
|
+
MobkitReconcileResult,
|
|
340
|
+
await self.request(
|
|
341
|
+
request_id,
|
|
342
|
+
"mobkit/reconcile",
|
|
343
|
+
{"modules": modules},
|
|
344
|
+
_is_reconcile_result,
|
|
345
|
+
),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
async def spawn_member(
|
|
349
|
+
self, module_id: str, request_id: str = "spawn_member"
|
|
350
|
+
) -> MobkitSpawnMemberResult:
|
|
351
|
+
return cast(
|
|
352
|
+
MobkitSpawnMemberResult,
|
|
353
|
+
await self.request(
|
|
354
|
+
request_id,
|
|
355
|
+
"mobkit/spawn_member",
|
|
356
|
+
{"module_id": module_id},
|
|
357
|
+
_is_spawn_member_result,
|
|
358
|
+
),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
async def subscribe_events(
|
|
362
|
+
self,
|
|
363
|
+
params: MobkitSubscribeParams | None = None,
|
|
364
|
+
request_id: str = "events_subscribe",
|
|
365
|
+
) -> MobkitSubscribeResult:
|
|
366
|
+
return cast(
|
|
367
|
+
MobkitSubscribeResult,
|
|
368
|
+
await self.request(
|
|
369
|
+
request_id,
|
|
370
|
+
"mobkit/events/subscribe",
|
|
371
|
+
dict(params) if params is not None else {},
|
|
372
|
+
_is_subscribe_result,
|
|
373
|
+
),
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _read_http_body(http_request: urllib_request.Request, timeout_seconds: float) -> str:
|
|
378
|
+
with urllib_request.urlopen(http_request, timeout=timeout_seconds) as response:
|
|
379
|
+
return response.read().decode("utf-8")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _build_request(
|
|
383
|
+
request_id: str,
|
|
384
|
+
method: str,
|
|
385
|
+
params: Mapping[str, Any] | None,
|
|
386
|
+
) -> JsonRpcRequest:
|
|
387
|
+
return {
|
|
388
|
+
"jsonrpc": "2.0",
|
|
389
|
+
"id": request_id,
|
|
390
|
+
"method": method,
|
|
391
|
+
"params": dict(params) if params is not None else {},
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _parse_json_rpc_response(payload: Any, request_id: str) -> JsonRpcResponse:
|
|
396
|
+
if not isinstance(payload, dict):
|
|
397
|
+
raise ValueError("invalid JSON-RPC response envelope")
|
|
398
|
+
if payload.get("jsonrpc") != "2.0" or payload.get("id") != request_id:
|
|
399
|
+
raise ValueError("invalid JSON-RPC response envelope")
|
|
400
|
+
|
|
401
|
+
has_result = "result" in payload
|
|
402
|
+
has_error = "error" in payload
|
|
403
|
+
if has_result == has_error:
|
|
404
|
+
raise ValueError("invalid JSON-RPC response envelope")
|
|
405
|
+
|
|
406
|
+
if has_error:
|
|
407
|
+
error = payload.get("error")
|
|
408
|
+
if not isinstance(error, dict):
|
|
409
|
+
raise ValueError("invalid JSON-RPC response envelope")
|
|
410
|
+
code = error.get("code")
|
|
411
|
+
message = error.get("message")
|
|
412
|
+
if not isinstance(code, int) or isinstance(code, bool):
|
|
413
|
+
raise ValueError("invalid JSON-RPC response envelope")
|
|
414
|
+
if not isinstance(message, str):
|
|
415
|
+
raise ValueError("invalid JSON-RPC response envelope")
|
|
416
|
+
|
|
417
|
+
return cast(JsonRpcResponse, payload)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _unwrap_typed_result(
|
|
421
|
+
response: JsonRpcResponse,
|
|
422
|
+
request_id: str,
|
|
423
|
+
method: str,
|
|
424
|
+
validator: Callable[[Any], bool],
|
|
425
|
+
) -> Any:
|
|
426
|
+
if "error" in response:
|
|
427
|
+
error = response["error"]
|
|
428
|
+
raise MobkitRpcError(error["code"], error["message"], request_id, method)
|
|
429
|
+
|
|
430
|
+
result = response["result"]
|
|
431
|
+
if not validator(result):
|
|
432
|
+
raise ValueError(f"invalid result payload for {method}")
|
|
433
|
+
return result
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _is_status_result(value: Any) -> bool:
|
|
437
|
+
return (
|
|
438
|
+
isinstance(value, dict)
|
|
439
|
+
and isinstance(value.get("contract_version"), str)
|
|
440
|
+
and isinstance(value.get("running"), bool)
|
|
441
|
+
and _is_string_list(value.get("loaded_modules"))
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _is_capabilities_result(value: Any) -> bool:
|
|
446
|
+
return (
|
|
447
|
+
isinstance(value, dict)
|
|
448
|
+
and isinstance(value.get("contract_version"), str)
|
|
449
|
+
and _is_string_list(value.get("methods"))
|
|
450
|
+
and _is_string_list(value.get("loaded_modules"))
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _is_reconcile_result(value: Any) -> bool:
|
|
455
|
+
return (
|
|
456
|
+
isinstance(value, dict)
|
|
457
|
+
and isinstance(value.get("accepted"), bool)
|
|
458
|
+
and _is_string_list(value.get("reconciled_modules"))
|
|
459
|
+
and isinstance(value.get("added"), int)
|
|
460
|
+
and not isinstance(value.get("added"), bool)
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _is_spawn_member_result(value: Any) -> bool:
|
|
465
|
+
return (
|
|
466
|
+
isinstance(value, dict)
|
|
467
|
+
and isinstance(value.get("accepted"), bool)
|
|
468
|
+
and isinstance(value.get("module_id"), str)
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _is_subscribe_result(value: Any) -> bool:
|
|
473
|
+
if not isinstance(value, dict):
|
|
474
|
+
return False
|
|
475
|
+
|
|
476
|
+
scope = value.get("scope")
|
|
477
|
+
if scope not in {"mob", "agent", "interaction"}:
|
|
478
|
+
return False
|
|
479
|
+
|
|
480
|
+
replay = value.get("replay_from_event_id")
|
|
481
|
+
if replay is not None and not isinstance(replay, str):
|
|
482
|
+
return False
|
|
483
|
+
|
|
484
|
+
keep_alive = value.get("keep_alive")
|
|
485
|
+
if not isinstance(keep_alive, dict):
|
|
486
|
+
return False
|
|
487
|
+
|
|
488
|
+
interval = keep_alive.get("interval_ms")
|
|
489
|
+
if not isinstance(interval, int) or isinstance(interval, bool):
|
|
490
|
+
return False
|
|
491
|
+
if not isinstance(keep_alive.get("event"), str):
|
|
492
|
+
return False
|
|
493
|
+
|
|
494
|
+
if not isinstance(value.get("keep_alive_comment"), str):
|
|
495
|
+
return False
|
|
496
|
+
|
|
497
|
+
if not _is_string_list(value.get("event_frames")):
|
|
498
|
+
return False
|
|
499
|
+
|
|
500
|
+
events = value.get("events")
|
|
501
|
+
if not isinstance(events, list):
|
|
502
|
+
return False
|
|
503
|
+
|
|
504
|
+
for event in events:
|
|
505
|
+
if not isinstance(event, dict):
|
|
506
|
+
return False
|
|
507
|
+
timestamp = event.get("timestamp_ms")
|
|
508
|
+
if (
|
|
509
|
+
not isinstance(event.get("event_id"), str)
|
|
510
|
+
or not isinstance(event.get("source"), str)
|
|
511
|
+
or not isinstance(timestamp, int)
|
|
512
|
+
or isinstance(timestamp, bool)
|
|
513
|
+
or "event" not in event
|
|
514
|
+
):
|
|
515
|
+
return False
|
|
516
|
+
|
|
517
|
+
return True
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _is_string_list(value: Any) -> bool:
|
|
521
|
+
return isinstance(value, list) and all(isinstance(item, str) for item in value)
|