mwps-client 0.1.7__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.
- mwps_client-0.1.7/PKG-INFO +43 -0
- mwps_client-0.1.7/README.md +36 -0
- mwps_client-0.1.7/VERSION +1 -0
- mwps_client-0.1.7/mwps_client/__init__.py +36 -0
- mwps_client-0.1.7/mwps_client/client.py +390 -0
- mwps_client-0.1.7/mwps_client.egg-info/PKG-INFO +43 -0
- mwps_client-0.1.7/mwps_client.egg-info/SOURCES.txt +9 -0
- mwps_client-0.1.7/mwps_client.egg-info/dependency_links.txt +1 -0
- mwps_client-0.1.7/mwps_client.egg-info/top_level.txt +1 -0
- mwps_client-0.1.7/pyproject.toml +24 -0
- mwps_client-0.1.7/setup.cfg +4 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mwps-client
|
|
3
|
+
Version: 0.1.7
|
|
4
|
+
Summary: Official mwps client library: single entry point MwpsClient, one typed method per mwps command.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# mwps-client
|
|
9
|
+
|
|
10
|
+
Official Python client for the `mwps` server. `MwpsClient` is the single
|
|
11
|
+
entry point: it hides transport selection, TLS material, proxy routing,
|
|
12
|
+
request correlation and error mapping behind one typed async method per
|
|
13
|
+
`mwps` command (see `mwps_client/mwps_client/client.py`).
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
pip install mwps-client
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import asyncio
|
|
25
|
+
from mwps_client import MwpsClient
|
|
26
|
+
|
|
27
|
+
async def main():
|
|
28
|
+
client = MwpsClient(base_url="https://<host>:8015", ca="/path/to/ca.crt")
|
|
29
|
+
result = await client.invoke_tool(tool_name="list_servers")
|
|
30
|
+
print(result)
|
|
31
|
+
|
|
32
|
+
asyncio.run(main())
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Version lockstep
|
|
36
|
+
|
|
37
|
+
`mwps-client` carries no version of its own: its published version is
|
|
38
|
+
always identical to the `mwps` server version it was generated against
|
|
39
|
+
(C-068, ClientServerVersionLockstep). Both packages read the same
|
|
40
|
+
repository-root `VERSION` file — `mwps_client/VERSION` is a symlink to
|
|
41
|
+
`../VERSION` — so a client published from a given deploy always matches
|
|
42
|
+
the server it talks to. See `pipeline/README.md` for the release process
|
|
43
|
+
that keeps the two in lockstep.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# mwps-client
|
|
2
|
+
|
|
3
|
+
Official Python client for the `mwps` server. `MwpsClient` is the single
|
|
4
|
+
entry point: it hides transport selection, TLS material, proxy routing,
|
|
5
|
+
request correlation and error mapping behind one typed async method per
|
|
6
|
+
`mwps` command (see `mwps_client/mwps_client/client.py`).
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
pip install mwps-client
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import asyncio
|
|
18
|
+
from mwps_client import MwpsClient
|
|
19
|
+
|
|
20
|
+
async def main():
|
|
21
|
+
client = MwpsClient(base_url="https://<host>:8015", ca="/path/to/ca.crt")
|
|
22
|
+
result = await client.invoke_tool(tool_name="list_servers")
|
|
23
|
+
print(result)
|
|
24
|
+
|
|
25
|
+
asyncio.run(main())
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Version lockstep
|
|
29
|
+
|
|
30
|
+
`mwps-client` carries no version of its own: its published version is
|
|
31
|
+
always identical to the `mwps` server version it was generated against
|
|
32
|
+
(C-068, ClientServerVersionLockstep). Both packages read the same
|
|
33
|
+
repository-root `VERSION` file — `mwps_client/VERSION` is a symlink to
|
|
34
|
+
`../VERSION` — so a client published from a given deploy always matches
|
|
35
|
+
the server it talks to. See `pipeline/README.md` for the release process
|
|
36
|
+
that keeps the two in lockstep.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.7
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from .client import MwpsClient
|
|
4
|
+
|
|
5
|
+
# Lockstep version (C-068, ClientServerVersionLockstep): must always equal
|
|
6
|
+
# the mwps server's [project].version in the repo-root pyproject.toml. Both
|
|
7
|
+
# read the same root VERSION file (mwps_client/pyproject.toml derives its
|
|
8
|
+
# published version from it via
|
|
9
|
+
# [tool.setuptools.dynamic] version = {file = "VERSION"}, and
|
|
10
|
+
# mwps_client/VERSION is a symlink to ../VERSION).
|
|
11
|
+
#
|
|
12
|
+
# When installed (wheel/sdist), the version setuptools recorded at build
|
|
13
|
+
# time is authoritative and is read via importlib.metadata -- no network,
|
|
14
|
+
# no filesystem lookup outside the installed distribution's own metadata.
|
|
15
|
+
# When running from a checkout without an installed distribution (e.g. a
|
|
16
|
+
# fresh `python -c "import mwps_client"` before `pip install -e .`), fall
|
|
17
|
+
# back to reading the VERSION file next to this package's pyproject.toml.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _read_version() -> str:
|
|
21
|
+
try:
|
|
22
|
+
from importlib.metadata import version
|
|
23
|
+
|
|
24
|
+
return version("mwps-client")
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
try:
|
|
28
|
+
version_file = Path(__file__).resolve().parent.parent / "VERSION"
|
|
29
|
+
return version_file.read_text(encoding="utf-8").strip()
|
|
30
|
+
except OSError:
|
|
31
|
+
return "0.0.0"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__version__ = _read_version()
|
|
35
|
+
|
|
36
|
+
__all__ = ["MwpsClient", "__version__"]
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mwps_client.client
|
|
3
|
+
|
|
4
|
+
MwpsClient: the sole entry point of the mwps-client package. Hides all
|
|
5
|
+
network interaction (transport selection, TLS material, proxy routing,
|
|
6
|
+
request correlation, transport-level retry, error mapping) behind one
|
|
7
|
+
async, typed method per mwps command.
|
|
8
|
+
|
|
9
|
+
Version note (C-068, ClientServerVersionLockstep): this module carries no
|
|
10
|
+
version string of its own. mwps-client's published version is always
|
|
11
|
+
identical to the mwps server version it was generated against and is
|
|
12
|
+
derived at build time; every server deployment obligatorily raises that
|
|
13
|
+
version, and mwps-client is republished to PyPI at the same new version in
|
|
14
|
+
the same delivery. Do not add a __version__ or literal version string here.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
from typing import Any, Dict, List, Optional, cast
|
|
21
|
+
from urllib.parse import urlparse
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MwpsClientError(Exception):
|
|
25
|
+
"""Transport-level error raised by MwpsClient; message is safe to display."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, message: str, status: Optional[int] = None) -> None:
|
|
28
|
+
"""Store error message and optional status code."""
|
|
29
|
+
super().__init__(message)
|
|
30
|
+
self.message = message
|
|
31
|
+
self.status = status
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MwpsClient:
|
|
35
|
+
"""
|
|
36
|
+
Single entry point for the mwps-client package.
|
|
37
|
+
|
|
38
|
+
Hides transport selection, TLS material, proxy routing, request
|
|
39
|
+
correlation, transport-level retry, and error mapping behind one typed
|
|
40
|
+
async method per mwps command. All commands are invoked as JSON-RPC
|
|
41
|
+
calls through mcp_proxy_adapter's JsonRpcClient, routed via mcp-proxy.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
base_url: str,
|
|
47
|
+
token_header: Optional[str] = None,
|
|
48
|
+
token: Optional[str] = None,
|
|
49
|
+
cert: Optional[str] = None,
|
|
50
|
+
key: Optional[str] = None,
|
|
51
|
+
ca: Optional[str] = None,
|
|
52
|
+
timeout: float = 30.0,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Initialize the client with mcp-proxy connection parameters.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
base_url: mcp-proxy base URL (http, https, ws, or wss).
|
|
59
|
+
token_header: Optional HTTP header name carrying the auth token.
|
|
60
|
+
token: Optional auth token value.
|
|
61
|
+
cert: Optional path to client TLS certificate (mTLS). Only
|
|
62
|
+
takes effect together with `key`.
|
|
63
|
+
key: Optional path to client TLS private key (mTLS). Only
|
|
64
|
+
takes effect together with `cert`.
|
|
65
|
+
ca: Optional path to CA certificate for server verification.
|
|
66
|
+
Forwarded to the adapter whenever supplied, independently
|
|
67
|
+
of `cert`/`key` -- plain `https` with a self-signed or
|
|
68
|
+
private CA needs `ca` alone, with no client certificate.
|
|
69
|
+
timeout: Request timeout in seconds.
|
|
70
|
+
"""
|
|
71
|
+
parsed = urlparse((base_url or "").rstrip("/"))
|
|
72
|
+
scheme = (parsed.scheme or "http").lower()
|
|
73
|
+
host = parsed.hostname or "localhost"
|
|
74
|
+
port = parsed.port
|
|
75
|
+
if port is None:
|
|
76
|
+
port = 443 if scheme in ("https", "wss") else 80
|
|
77
|
+
protocol = "https" if scheme in ("https", "wss") else "http"
|
|
78
|
+
self._adapter_params: Dict[str, Any] = {
|
|
79
|
+
"protocol": protocol,
|
|
80
|
+
"host": host,
|
|
81
|
+
"port": port,
|
|
82
|
+
"token_header": token_header,
|
|
83
|
+
"token": token,
|
|
84
|
+
"timeout": timeout,
|
|
85
|
+
}
|
|
86
|
+
if cert and key:
|
|
87
|
+
self._adapter_params["cert"] = cert
|
|
88
|
+
self._adapter_params["key"] = key
|
|
89
|
+
if ca:
|
|
90
|
+
self._adapter_params["ca"] = ca
|
|
91
|
+
self._client: Any = None
|
|
92
|
+
self._client_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
93
|
+
|
|
94
|
+
async def _get_client(self) -> Any:
|
|
95
|
+
"""Lazily create and cache the underlying JsonRpcClient for the running loop.
|
|
96
|
+
|
|
97
|
+
A cached adapter client holds an httpx.AsyncClient whose pooled
|
|
98
|
+
connections and internal async primitives are bound to the event
|
|
99
|
+
loop that was running when they were created. mwps-client is
|
|
100
|
+
typically driven by pipeline/runner.py, which invokes each check
|
|
101
|
+
through its own ``asyncio.run(...)`` call -- a fresh event loop
|
|
102
|
+
every time. Reusing the same adapter client across loops makes its
|
|
103
|
+
first request in a new loop fail with "Event loop is closed" (the
|
|
104
|
+
pooled connection tries to schedule I/O on the now-closed old
|
|
105
|
+
loop), even though the client itself is fine and later requests in
|
|
106
|
+
that same loop succeed once the stale connection is dropped.
|
|
107
|
+
|
|
108
|
+
To stay correct across any number of ``asyncio.run()`` calls, close
|
|
109
|
+
and drop the cached client whenever the running loop has changed
|
|
110
|
+
since it was created, then build a fresh one for the current loop.
|
|
111
|
+
"""
|
|
112
|
+
running_loop = asyncio.get_running_loop()
|
|
113
|
+
if self._client is not None and self._client_loop is running_loop:
|
|
114
|
+
return self._client
|
|
115
|
+
if self._client is not None:
|
|
116
|
+
await self._close_adapter_client(self._client)
|
|
117
|
+
try:
|
|
118
|
+
from mcp_proxy_adapter.client.jsonrpc_client import JsonRpcClient
|
|
119
|
+
except ImportError as e:
|
|
120
|
+
raise MwpsClientError(
|
|
121
|
+
f"mcp_proxy_adapter client not available: {e}"
|
|
122
|
+
) from e
|
|
123
|
+
self._client = JsonRpcClient(**self._adapter_params)
|
|
124
|
+
self._client_loop = running_loop
|
|
125
|
+
return self._client
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
async def _close_adapter_client(client: Any) -> None:
|
|
129
|
+
"""Best-effort close of a stale adapter client; never raises."""
|
|
130
|
+
close = getattr(client, "close", None)
|
|
131
|
+
if not callable(close):
|
|
132
|
+
return
|
|
133
|
+
try:
|
|
134
|
+
result = close()
|
|
135
|
+
if asyncio.iscoroutine(result):
|
|
136
|
+
await result
|
|
137
|
+
except Exception: # noqa: BLE001 -- teardown failure must not mask real errors
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
async def aclose(self) -> None:
|
|
141
|
+
"""Close the underlying adapter client, if one was created.
|
|
142
|
+
|
|
143
|
+
Safe to call from any running loop, and safe to call more than
|
|
144
|
+
once. Intended for callers (such as pipeline/main.py) that want to
|
|
145
|
+
release the transport deterministically after use instead of
|
|
146
|
+
relying on lazy per-loop recreation in ``_get_client``.
|
|
147
|
+
"""
|
|
148
|
+
if self._client is None:
|
|
149
|
+
return
|
|
150
|
+
await self._close_adapter_client(self._client)
|
|
151
|
+
self._client = None
|
|
152
|
+
self._client_loop = None
|
|
153
|
+
|
|
154
|
+
async def _call_command(self, command: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
155
|
+
"""
|
|
156
|
+
Invoke one mwps command via JSON-RPC and return its result.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
MwpsClientError: on any transport or protocol failure.
|
|
160
|
+
"""
|
|
161
|
+
client = await self._get_client()
|
|
162
|
+
try:
|
|
163
|
+
response = await client.jsonrpc_call(command, params)
|
|
164
|
+
return cast(Dict[str, Any], client._extract_result(response))
|
|
165
|
+
except MwpsClientError:
|
|
166
|
+
raise
|
|
167
|
+
except Exception as e: # noqa: BLE001
|
|
168
|
+
resp = getattr(e, "response", None)
|
|
169
|
+
status = (
|
|
170
|
+
getattr(e, "status_code", None)
|
|
171
|
+
or getattr(e, "status", None)
|
|
172
|
+
or (getattr(resp, "status_code", None) if resp is not None else None)
|
|
173
|
+
)
|
|
174
|
+
raise MwpsClientError(str(e), status=status) from e
|
|
175
|
+
|
|
176
|
+
async def session_init(
|
|
177
|
+
self,
|
|
178
|
+
model: Optional[str] = None,
|
|
179
|
+
allowed_commands: Optional[List[str]] = None,
|
|
180
|
+
forbidden_commands: Optional[List[str]] = None,
|
|
181
|
+
standards: Optional[List[str]] = None,
|
|
182
|
+
session_rules: Optional[List[str]] = None,
|
|
183
|
+
) -> Dict[str, Any]:
|
|
184
|
+
"""
|
|
185
|
+
Create a new chat session and return its identifier.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
model: Default MWPS model id to associate with the session.
|
|
189
|
+
allowed_commands: Allow-list of command names for the session.
|
|
190
|
+
forbidden_commands: Forbid-list of command names for the session.
|
|
191
|
+
standards: Canonical standards text blocks for session context.
|
|
192
|
+
session_rules: Session rules text blocks for session context.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Object with session_id (string, UUID4).
|
|
196
|
+
"""
|
|
197
|
+
params: Dict[str, Any] = {}
|
|
198
|
+
if model is not None:
|
|
199
|
+
params["model"] = model
|
|
200
|
+
if allowed_commands is not None:
|
|
201
|
+
params["allowed_commands"] = allowed_commands
|
|
202
|
+
if forbidden_commands is not None:
|
|
203
|
+
params["forbidden_commands"] = forbidden_commands
|
|
204
|
+
if standards is not None:
|
|
205
|
+
params["standards"] = standards
|
|
206
|
+
if session_rules is not None:
|
|
207
|
+
params["session_rules"] = session_rules
|
|
208
|
+
return await self._call_command("session_init", params)
|
|
209
|
+
|
|
210
|
+
async def session_update(
|
|
211
|
+
self,
|
|
212
|
+
session_id: str,
|
|
213
|
+
model: Optional[str] = None,
|
|
214
|
+
allowed_commands: Optional[List[str]] = None,
|
|
215
|
+
forbidden_commands: Optional[List[str]] = None,
|
|
216
|
+
standards: Optional[List[str]] = None,
|
|
217
|
+
session_rules: Optional[List[str]] = None,
|
|
218
|
+
) -> Dict[str, Any]:
|
|
219
|
+
"""
|
|
220
|
+
Update an existing session in place (full replacement of supplied lists).
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
session_id: UUID4 of the session to update.
|
|
224
|
+
model: Sets the session's default MWPS model.
|
|
225
|
+
allowed_commands: Replaces the session's allow-list.
|
|
226
|
+
forbidden_commands: Replaces the session's forbid-list.
|
|
227
|
+
standards: Replaces the session's standards context blocks.
|
|
228
|
+
session_rules: Replaces the session's session-rules context blocks.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Object with ok (boolean).
|
|
232
|
+
"""
|
|
233
|
+
params: Dict[str, Any] = {"session_id": session_id}
|
|
234
|
+
if model is not None:
|
|
235
|
+
params["model"] = model
|
|
236
|
+
if allowed_commands is not None:
|
|
237
|
+
params["allowed_commands"] = allowed_commands
|
|
238
|
+
if forbidden_commands is not None:
|
|
239
|
+
params["forbidden_commands"] = forbidden_commands
|
|
240
|
+
if standards is not None:
|
|
241
|
+
params["standards"] = standards
|
|
242
|
+
if session_rules is not None:
|
|
243
|
+
params["session_rules"] = session_rules
|
|
244
|
+
return await self._call_command("session_update", params)
|
|
245
|
+
|
|
246
|
+
async def add_command_to_session(
|
|
247
|
+
self,
|
|
248
|
+
session_id: str,
|
|
249
|
+
command_id: str,
|
|
250
|
+
) -> Dict[str, Any]:
|
|
251
|
+
"""
|
|
252
|
+
Grant a session access to one additional command.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
session_id: UUID4 of the session.
|
|
256
|
+
command_id: Name of the command to allow.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Object with ok (boolean).
|
|
260
|
+
"""
|
|
261
|
+
params: Dict[str, Any] = {"session_id": session_id, "command_id": command_id}
|
|
262
|
+
return await self._call_command("add_command_to_session", params)
|
|
263
|
+
|
|
264
|
+
async def remove_command_from_session(
|
|
265
|
+
self,
|
|
266
|
+
session_id: str,
|
|
267
|
+
command_id: str,
|
|
268
|
+
) -> Dict[str, Any]:
|
|
269
|
+
"""
|
|
270
|
+
Revoke a session's access to one command.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
session_id: UUID4 of the session.
|
|
274
|
+
command_id: Name of the command to remove.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Object with ok (boolean).
|
|
278
|
+
"""
|
|
279
|
+
params: Dict[str, Any] = {"session_id": session_id, "command_id": command_id}
|
|
280
|
+
return await self._call_command("remove_command_from_session", params)
|
|
281
|
+
|
|
282
|
+
async def get_model_context(
|
|
283
|
+
self,
|
|
284
|
+
session_id: str,
|
|
285
|
+
content: Optional[str] = None,
|
|
286
|
+
) -> Dict[str, Any]:
|
|
287
|
+
"""
|
|
288
|
+
Return the exact final context that would be sent to the model.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
session_id: UUID4 of the session.
|
|
292
|
+
content: User message to append as the final message.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Object with final_context, representation_model, messages,
|
|
296
|
+
tool_names, tools, messages_count.
|
|
297
|
+
"""
|
|
298
|
+
params: Dict[str, Any] = {"session_id": session_id}
|
|
299
|
+
if content is not None:
|
|
300
|
+
params["content"] = content
|
|
301
|
+
return await self._call_command("get_model_context", params)
|
|
302
|
+
|
|
303
|
+
async def invoke_tool(
|
|
304
|
+
self,
|
|
305
|
+
tool_name: str,
|
|
306
|
+
arguments: Optional[Dict[str, Any]] = None,
|
|
307
|
+
) -> Dict[str, Any]:
|
|
308
|
+
"""
|
|
309
|
+
Invoke one proxy-registered tool the way the model would invoke it.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
tool_name: The tool's display name, or a proxy meta-command
|
|
313
|
+
(list_servers, call_server, help).
|
|
314
|
+
arguments: Arguments passed to the tool.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Object with content (string).
|
|
318
|
+
"""
|
|
319
|
+
params: Dict[str, Any] = {"tool_name": tool_name}
|
|
320
|
+
if arguments is not None:
|
|
321
|
+
params["arguments"] = arguments
|
|
322
|
+
return await self._call_command("invoke_tool", params)
|
|
323
|
+
|
|
324
|
+
async def info(
|
|
325
|
+
self,
|
|
326
|
+
section: Optional[str] = None,
|
|
327
|
+
) -> Dict[str, Any]:
|
|
328
|
+
"""
|
|
329
|
+
Return server documentation: overview + table of contents, or one section.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
section: Documentation section name (e.g. "commands",
|
|
333
|
+
"configuration"); omit for the overview and table of
|
|
334
|
+
contents.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Without section: object with description, server (name,
|
|
338
|
+
version), sections (array of name/title/summary). With
|
|
339
|
+
section: object with section, title, content.
|
|
340
|
+
"""
|
|
341
|
+
params: Dict[str, Any] = {}
|
|
342
|
+
if section is not None:
|
|
343
|
+
params["section"] = section
|
|
344
|
+
return await self._call_command("info", params)
|
|
345
|
+
|
|
346
|
+
async def mwps_chat(
|
|
347
|
+
self,
|
|
348
|
+
messages: Optional[List[Dict[str, str]]] = None,
|
|
349
|
+
model: Optional[str] = None,
|
|
350
|
+
stream: Optional[bool] = None,
|
|
351
|
+
max_tool_rounds: Optional[int] = None,
|
|
352
|
+
config_path: Optional[str] = None,
|
|
353
|
+
session_id: Optional[str] = None,
|
|
354
|
+
content: Optional[str] = None,
|
|
355
|
+
) -> Dict[str, Any]:
|
|
356
|
+
"""
|
|
357
|
+
Run one chat turn against MWPS, in direct mode or session mode.
|
|
358
|
+
|
|
359
|
+
Direct mode: supply `messages`. Session mode: supply `session_id`
|
|
360
|
+
together with `content`. Exactly one mode must be used per call.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
messages: Chat messages for direct mode; each item has role
|
|
364
|
+
and content.
|
|
365
|
+
model: MWPS model name overriding the session/config default.
|
|
366
|
+
stream: If True, the reply is streamed.
|
|
367
|
+
max_tool_rounds: Maximum number of tool-call rounds.
|
|
368
|
+
config_path: Path to the adapter/workstation configuration file.
|
|
369
|
+
session_id: UUID4 of the session, for session mode.
|
|
370
|
+
content: New user message text for session mode.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Object with message (string) and history (array of object).
|
|
374
|
+
"""
|
|
375
|
+
params: Dict[str, Any] = {}
|
|
376
|
+
if messages is not None:
|
|
377
|
+
params["messages"] = messages
|
|
378
|
+
if model is not None:
|
|
379
|
+
params["model"] = model
|
|
380
|
+
if stream is not None:
|
|
381
|
+
params["stream"] = stream
|
|
382
|
+
if max_tool_rounds is not None:
|
|
383
|
+
params["max_tool_rounds"] = max_tool_rounds
|
|
384
|
+
if config_path is not None:
|
|
385
|
+
params["config_path"] = config_path
|
|
386
|
+
if session_id is not None:
|
|
387
|
+
params["session_id"] = session_id
|
|
388
|
+
if content is not None:
|
|
389
|
+
params["content"] = content
|
|
390
|
+
return await self._call_command("mwps_chat", params)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mwps-client
|
|
3
|
+
Version: 0.1.7
|
|
4
|
+
Summary: Official mwps client library: single entry point MwpsClient, one typed method per mwps command.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# mwps-client
|
|
9
|
+
|
|
10
|
+
Official Python client for the `mwps` server. `MwpsClient` is the single
|
|
11
|
+
entry point: it hides transport selection, TLS material, proxy routing,
|
|
12
|
+
request correlation and error mapping behind one typed async method per
|
|
13
|
+
`mwps` command (see `mwps_client/mwps_client/client.py`).
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
pip install mwps-client
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import asyncio
|
|
25
|
+
from mwps_client import MwpsClient
|
|
26
|
+
|
|
27
|
+
async def main():
|
|
28
|
+
client = MwpsClient(base_url="https://<host>:8015", ca="/path/to/ca.crt")
|
|
29
|
+
result = await client.invoke_tool(tool_name="list_servers")
|
|
30
|
+
print(result)
|
|
31
|
+
|
|
32
|
+
asyncio.run(main())
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Version lockstep
|
|
36
|
+
|
|
37
|
+
`mwps-client` carries no version of its own: its published version is
|
|
38
|
+
always identical to the `mwps` server version it was generated against
|
|
39
|
+
(C-068, ClientServerVersionLockstep). Both packages read the same
|
|
40
|
+
repository-root `VERSION` file — `mwps_client/VERSION` is a symlink to
|
|
41
|
+
`../VERSION` — so a client published from a given deploy always matches
|
|
42
|
+
the server it talks to. See `pipeline/README.md` for the release process
|
|
43
|
+
that keeps the two in lockstep.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mwps_client
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mwps-client"
|
|
7
|
+
# Per C-068 (ClientServerVersionLockstep), this client carries no static
|
|
8
|
+
# version of its own. Its version is always identical to the mwps server
|
|
9
|
+
# version and is derived and republished at every server deployment; it
|
|
10
|
+
# must never be hardcoded here as a literal version string.
|
|
11
|
+
dynamic = ["version"]
|
|
12
|
+
description = "Official mwps client library: single entry point MwpsClient, one typed method per mwps command."
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
|
|
16
|
+
[tool.setuptools]
|
|
17
|
+
packages = ["mwps_client"]
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.dynamic]
|
|
20
|
+
# Single source of truth is the repo-root VERSION file (mwps_client/VERSION
|
|
21
|
+
# is a symlink to ../VERSION), the same file the mwps server's root
|
|
22
|
+
# pyproject.toml reads (C-068, ClientServerVersionLockstep). See
|
|
23
|
+
# pipeline/README.md for the build-time lockstep requirement.
|
|
24
|
+
version = {file = "VERSION"}
|