@aliwey/bmo 2.0.0
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.
- package/README.md +90 -0
- package/bin/bmo.js +188 -0
- package/cli.py +1129 -0
- package/config/__init__.py +0 -0
- package/config/__pycache__/__init__.cpython-313.pyc +0 -0
- package/config/__pycache__/settings.cpython-313.pyc +0 -0
- package/config/__pycache__/system-prompt.cpython-313.pyc +0 -0
- package/config/settings.py +104 -0
- package/config/system-prompt.json +18 -0
- package/core/__init__.py +0 -0
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_a2a_bridge.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_agent.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_agent_card.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_connector.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_discovery.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_identity.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_tasks.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_transport.cpython-313.pyc +0 -0
- package/core/__pycache__/bmo_engine.cpython-313.pyc +0 -0
- package/core/__pycache__/bot_client.cpython-313.pyc +0 -0
- package/core/__pycache__/budget_tracker.cpython-313.pyc +0 -0
- package/core/__pycache__/cli_renderer.cpython-313.pyc +0 -0
- package/core/__pycache__/goal_runner.cpython-313.pyc +0 -0
- package/core/__pycache__/request_worker.cpython-313.pyc +0 -0
- package/core/__pycache__/security.cpython-313.pyc +0 -0
- package/core/__pycache__/shared_state.cpython-313.pyc +0 -0
- package/core/__pycache__/worker_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/worker_multiproc.cpython-313.pyc +0 -0
- package/core/__pycache__/worker_protocol.cpython-313.pyc +0 -0
- package/core/__pycache__/worker_subprocess.cpython-313.pyc +0 -0
- package/core/bfp_a2a_bridge.py +399 -0
- package/core/bfp_agent.py +98 -0
- package/core/bfp_agent_card.py +161 -0
- package/core/bfp_connector.py +177 -0
- package/core/bfp_discovery.py +105 -0
- package/core/bfp_identity.py +83 -0
- package/core/bfp_tasks.py +70 -0
- package/core/bfp_transport.py +368 -0
- package/core/bmo_engine.py +405 -0
- package/core/bot_client.py +838 -0
- package/core/budget_tracker.py +62 -0
- package/core/cli_renderer.py +177 -0
- package/core/goal_runner.py +129 -0
- package/core/request_worker.py +242 -0
- package/core/security.py +42 -0
- package/core/shared_state.py +4 -0
- package/core/worker_manager.py +71 -0
- package/core/worker_multiproc.py +155 -0
- package/core/worker_protocol.py +30 -0
- package/core/worker_subprocess.py +222 -0
- package/handlers/__init__.py +0 -0
- package/handlers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/handlers/__pycache__/messages.cpython-313.pyc +0 -0
- package/handlers/messages.py +2761 -0
- package/main.py +125 -0
- package/memory.md +43 -0
- package/models/__init__.py +0 -0
- package/models/__pycache__/__init__.cpython-313.pyc +0 -0
- package/models/__pycache__/chat_models.cpython-313.pyc +0 -0
- package/models/chat_models.py +143 -0
- package/package.json +50 -0
- package/registry/worker.js +108 -0
- package/registry/wrangler.toml +11 -0
- package/requirements.txt +13 -0
- package/scripts/bmo_init.js +115 -0
- package/scripts/postinstall.js +265 -0
- package/scripts/relay_cmd.js +276 -0
- package/scripts/web_cmd.js +136 -0
- package/setup.py +26 -0
- package/storage/__init__.py +0 -0
- package/storage/__pycache__/__init__.cpython-313.pyc +0 -0
- package/storage/__pycache__/sqlite_storage.cpython-313.pyc +0 -0
- package/storage/__pycache__/storage.cpython-313.pyc +0 -0
- package/storage/sqlite_storage.py +658 -0
- package/storage/storage.py +265 -0
- package/tools/__pycache__/bfp_relay.cpython-313.pyc +0 -0
- package/tools/__pycache__/get_session_summaries.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_bridge.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_server.cpython-313.pyc +0 -0
- package/tools/__pycache__/run_mcp_standalone.cpython-313.pyc +0 -0
- package/tools/__pycache__/task_registry.cpython-313.pyc +0 -0
- package/tools/__pycache__/test_mcp_connection.cpython-313.pyc +0 -0
- package/tools/bfp_relay.py +359 -0
- package/tools/bot.db +0 -0
- package/tools/get_session_summaries.py +45 -0
- package/tools/mcp_bridge.py +109 -0
- package/tools/mcp_server.py +531 -0
- package/tools/register_mcp_task.py +20 -0
- package/tools/run_detached.bat +32 -0
- package/tools/run_mcp_standalone.py +16 -0
- package/tools/task_registry.py +184 -0
- package/tools/test_mcp_connection.py +80 -0
- package/webchat/package-lock.json +1528 -0
- package/webchat/package.json +12 -0
- package/webchat/public/app.js +1293 -0
- package/webchat/public/index.html +226 -0
- package/webchat/public/index.html.bak +416 -0
- package/webchat/public/styles.css +2435 -0
- package/webchat/server.js +645 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
import threading
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import websockets
|
|
11
|
+
from websockets.asyncio.server import ServerConnection, serve as ws_serve
|
|
12
|
+
from websockets.http11 import Headers, Response
|
|
13
|
+
|
|
14
|
+
from core.bfp_identity import get_did, sign, verify
|
|
15
|
+
from core.bfp_tasks import create_task, get_task, update_task
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
BFP_METHODS = frozenset({
|
|
20
|
+
"bfp.discover",
|
|
21
|
+
"bfp.connect",
|
|
22
|
+
"bfp.delegate",
|
|
23
|
+
"bfp.status",
|
|
24
|
+
"bfp.cancel",
|
|
25
|
+
"bfp.ping",
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _make_rpc_message(method: str, params: dict, msg_id: Optional[str] = None) -> dict:
|
|
30
|
+
if msg_id is None:
|
|
31
|
+
msg_id = f"msg-{uuid.uuid4().hex[:8]}"
|
|
32
|
+
signed = sign(dict(params))
|
|
33
|
+
signature = signed.pop("signature", None)
|
|
34
|
+
message = {
|
|
35
|
+
"jsonrpc": "2.0",
|
|
36
|
+
"id": msg_id,
|
|
37
|
+
"method": method,
|
|
38
|
+
"params": params,
|
|
39
|
+
}
|
|
40
|
+
if signature:
|
|
41
|
+
message["signature"] = signature
|
|
42
|
+
return message
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _verify_message(remote_did: str, message: dict) -> bool:
|
|
46
|
+
if "signature" not in message:
|
|
47
|
+
return False
|
|
48
|
+
sig = message["signature"]
|
|
49
|
+
params = message.get("params", {})
|
|
50
|
+
check_data = dict(params)
|
|
51
|
+
check_data["signature"] = sig
|
|
52
|
+
return verify(remote_did, check_data)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _make_error(msg_id, code: int, message_text: str):
|
|
56
|
+
return {
|
|
57
|
+
"jsonrpc": "2.0",
|
|
58
|
+
"id": msg_id,
|
|
59
|
+
"error": {"code": code, "message": message_text},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class BFPRequest:
|
|
64
|
+
def __init__(self, message: dict, connection: "ServerConnection"):
|
|
65
|
+
self.message = message
|
|
66
|
+
self.connection = connection
|
|
67
|
+
self.method = message.get("method", "")
|
|
68
|
+
self.msg_id = message.get("id")
|
|
69
|
+
self.params = message.get("params", {})
|
|
70
|
+
|
|
71
|
+
async def reply(self, result: dict):
|
|
72
|
+
response = {"jsonrpc": "2.0", "id": self.msg_id, "result": result}
|
|
73
|
+
await self.connection.send(json.dumps(response))
|
|
74
|
+
|
|
75
|
+
async def reply_error(self, code: int, message_text: str, data: dict = None):
|
|
76
|
+
error = {"code": code, "message": message_text}
|
|
77
|
+
if data:
|
|
78
|
+
error["data"] = data
|
|
79
|
+
response = {"jsonrpc": "2.0", "id": self.msg_id, "error": error}
|
|
80
|
+
await self.connection.send(json.dumps(response))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class BFPServer:
|
|
84
|
+
def __init__(self, host: str = "0.0.0.0", port: int = 8765):
|
|
85
|
+
self.host = host
|
|
86
|
+
self.port = port
|
|
87
|
+
self._server = None
|
|
88
|
+
self._thread = None
|
|
89
|
+
self._loop = None
|
|
90
|
+
self._request_queue = asyncio.Queue()
|
|
91
|
+
self._running = False
|
|
92
|
+
self._actual_port = port
|
|
93
|
+
self._connections: dict[str, tuple] = {}
|
|
94
|
+
|
|
95
|
+
def get_port(self) -> int:
|
|
96
|
+
return self._actual_port
|
|
97
|
+
|
|
98
|
+
async def _handle_method(self, request: BFPRequest):
|
|
99
|
+
method = request.method
|
|
100
|
+
params = request.params
|
|
101
|
+
logger.info(f"Handling {method} from {params.get('from', 'unknown')}")
|
|
102
|
+
|
|
103
|
+
if method == "bfp.discover":
|
|
104
|
+
await request.reply({
|
|
105
|
+
"agent": "BMO",
|
|
106
|
+
"did": get_did(),
|
|
107
|
+
"version": "1.0.0",
|
|
108
|
+
"methods": sorted(BFP_METHODS),
|
|
109
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
110
|
+
})
|
|
111
|
+
elif method == "bfp.ping":
|
|
112
|
+
await request.reply({"pong": True, "timestamp": datetime.now(timezone.utc).isoformat()})
|
|
113
|
+
elif method == "bfp.connect":
|
|
114
|
+
session_id = f"ses-{uuid.uuid4().hex[:12]}"
|
|
115
|
+
remote_did = params.get("from", "")
|
|
116
|
+
await request.reply({
|
|
117
|
+
"session_id": session_id,
|
|
118
|
+
"status": "connected",
|
|
119
|
+
"remote_did": remote_did,
|
|
120
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
121
|
+
})
|
|
122
|
+
elif method == "bfp.delegate":
|
|
123
|
+
task = params.get("task", {})
|
|
124
|
+
source_did = params.get("from", "unknown")
|
|
125
|
+
task_id = create_task(task, source_did)
|
|
126
|
+
task_data = get_task(task_id)
|
|
127
|
+
await request.reply({
|
|
128
|
+
"task_id": task_id,
|
|
129
|
+
"status": task_data["status"] if task_data else "error",
|
|
130
|
+
"result": task_data.get("result") if task_data else None,
|
|
131
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
132
|
+
})
|
|
133
|
+
elif method == "bfp.status":
|
|
134
|
+
task_id = params.get("task_id", "")
|
|
135
|
+
task_data = get_task(task_id)
|
|
136
|
+
if task_data:
|
|
137
|
+
await request.reply({
|
|
138
|
+
"task_id": task_id,
|
|
139
|
+
"status": task_data["status"],
|
|
140
|
+
"result": task_data.get("result"),
|
|
141
|
+
"error": task_data.get("error"),
|
|
142
|
+
"created_at": task_data["created_at"],
|
|
143
|
+
"updated_at": task_data["updated_at"],
|
|
144
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
145
|
+
})
|
|
146
|
+
else:
|
|
147
|
+
await request.reply({
|
|
148
|
+
"task_id": task_id,
|
|
149
|
+
"status": "not_found",
|
|
150
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
151
|
+
})
|
|
152
|
+
elif method == "bfp.cancel":
|
|
153
|
+
task_id = params.get("task_id", "")
|
|
154
|
+
task_data = get_task(task_id)
|
|
155
|
+
if task_data and task_data["status"] in ("pending", "processing"):
|
|
156
|
+
update_task(task_id, "cancelled")
|
|
157
|
+
await request.reply({
|
|
158
|
+
"task_id": task_id,
|
|
159
|
+
"status": "cancelled",
|
|
160
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
161
|
+
})
|
|
162
|
+
else:
|
|
163
|
+
await request.reply({
|
|
164
|
+
"task_id": task_id,
|
|
165
|
+
"status": task_data["status"] if task_data else "not_found",
|
|
166
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
167
|
+
})
|
|
168
|
+
else:
|
|
169
|
+
await request.reply_error(-32601, f"Method not found: {method}")
|
|
170
|
+
|
|
171
|
+
async def _handler(self, conn: ServerConnection):
|
|
172
|
+
remote_did = None
|
|
173
|
+
conn_id = str(id(conn))
|
|
174
|
+
try:
|
|
175
|
+
async for raw in conn:
|
|
176
|
+
try:
|
|
177
|
+
message = json.loads(raw)
|
|
178
|
+
except json.JSONDecodeError:
|
|
179
|
+
await conn.send(json.dumps(_make_error(None, -32700, "Parse error")))
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
if not isinstance(message, dict) or message.get("jsonrpc") != "2.0":
|
|
183
|
+
msg_id = message.get("id") if isinstance(message, dict) else None
|
|
184
|
+
await conn.send(json.dumps(_make_error(msg_id, -32600, "Invalid Request")))
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
method = message.get("method", "")
|
|
188
|
+
if method not in BFP_METHODS:
|
|
189
|
+
await conn.send(json.dumps(_make_error(message.get("id"), -32601, f"Method not found: {method}")))
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
params = message.get("params", {})
|
|
193
|
+
remote_did = params.get("from", remote_did)
|
|
194
|
+
|
|
195
|
+
if remote_did and method not in ("bfp.discover",):
|
|
196
|
+
if not _verify_message(remote_did, message):
|
|
197
|
+
await conn.send(json.dumps(_make_error(message.get("id"), -32000, "Signature verification failed")))
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
if remote_did:
|
|
201
|
+
self._connections[conn_id] = (conn, remote_did)
|
|
202
|
+
|
|
203
|
+
request = BFPRequest(message, conn)
|
|
204
|
+
await self._handle_method(request)
|
|
205
|
+
|
|
206
|
+
except websockets.exceptions.ConnectionClosed:
|
|
207
|
+
pass
|
|
208
|
+
finally:
|
|
209
|
+
self._connections.pop(conn_id, None)
|
|
210
|
+
|
|
211
|
+
async def _process_request(self, conn, request):
|
|
212
|
+
connection_header = request.headers.get("Connection", "")
|
|
213
|
+
if "upgrade" not in connection_header.lower():
|
|
214
|
+
from websockets.http11 import Response
|
|
215
|
+
return Response(426, "Upgrade Required", Headers({"Content-Type": "text/plain"}), b"WebSocket endpoint - use ws:// scheme")
|
|
216
|
+
|
|
217
|
+
async def _run_server(self):
|
|
218
|
+
self._server = await ws_serve(
|
|
219
|
+
self._handler,
|
|
220
|
+
self.host,
|
|
221
|
+
self.port,
|
|
222
|
+
process_request=self._process_request,
|
|
223
|
+
)
|
|
224
|
+
sockname = self._server.sockets[0].getsockname() if self._server.sockets else None
|
|
225
|
+
if sockname:
|
|
226
|
+
self._actual_port = sockname[1]
|
|
227
|
+
logger.info(f"BFP Server listening on ws://{self.host}:{self._actual_port}")
|
|
228
|
+
await self._server.serve_forever()
|
|
229
|
+
|
|
230
|
+
def start(self):
|
|
231
|
+
if self._running:
|
|
232
|
+
return
|
|
233
|
+
self._running = True
|
|
234
|
+
|
|
235
|
+
def _run():
|
|
236
|
+
self._loop = asyncio.new_event_loop()
|
|
237
|
+
asyncio.set_event_loop(self._loop)
|
|
238
|
+
self._loop.run_until_complete(self._run_server())
|
|
239
|
+
|
|
240
|
+
self._thread = threading.Thread(target=_run, daemon=True, name="bfp-server")
|
|
241
|
+
self._thread.start()
|
|
242
|
+
logger.info("BFP Server thread started")
|
|
243
|
+
|
|
244
|
+
async def stop(self):
|
|
245
|
+
self._running = False
|
|
246
|
+
if self._server:
|
|
247
|
+
self._server.close()
|
|
248
|
+
if self._loop:
|
|
249
|
+
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
250
|
+
if self._thread:
|
|
251
|
+
self._thread.join(timeout=3)
|
|
252
|
+
|
|
253
|
+
async def get_request(self, timeout: float = 1.0) -> Optional[BFPRequest]:
|
|
254
|
+
try:
|
|
255
|
+
return await asyncio.wait_for(self._request_queue.get(), timeout=timeout)
|
|
256
|
+
except asyncio.TimeoutError:
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class BFPClient:
|
|
261
|
+
def __init__(self, endpoint: str, remote_did: str):
|
|
262
|
+
self.endpoint = endpoint
|
|
263
|
+
self.remote_did = remote_did
|
|
264
|
+
self._conn = None
|
|
265
|
+
self._reader_task = None
|
|
266
|
+
self._incoming = asyncio.Queue()
|
|
267
|
+
self._running = False
|
|
268
|
+
|
|
269
|
+
async def connect(self):
|
|
270
|
+
self._conn = await websockets.connect(self.endpoint)
|
|
271
|
+
self._running = True
|
|
272
|
+
self._reader_task = asyncio.create_task(self._reader())
|
|
273
|
+
logger.info(f"BFP Client connected to {self.endpoint}")
|
|
274
|
+
return True
|
|
275
|
+
|
|
276
|
+
async def _reader(self):
|
|
277
|
+
try:
|
|
278
|
+
async for raw in self._conn:
|
|
279
|
+
try:
|
|
280
|
+
msg = json.loads(raw)
|
|
281
|
+
await self._incoming.put(msg)
|
|
282
|
+
except json.JSONDecodeError:
|
|
283
|
+
logger.warning(f"Client received malformed JSON: {raw[:200]}")
|
|
284
|
+
except websockets.exceptions.ConnectionClosed:
|
|
285
|
+
pass
|
|
286
|
+
finally:
|
|
287
|
+
self._running = False
|
|
288
|
+
|
|
289
|
+
async def _send_and_wait(self, method: str, params: dict, timeout: float = 30.0) -> dict:
|
|
290
|
+
if not self._conn:
|
|
291
|
+
raise RuntimeError("Not connected. Call connect() first.")
|
|
292
|
+
|
|
293
|
+
msg_id = f"msg-{uuid.uuid4().hex[:8]}"
|
|
294
|
+
message = _make_rpc_message(method, params, msg_id)
|
|
295
|
+
await self._conn.send(json.dumps(message))
|
|
296
|
+
|
|
297
|
+
deadline = time.monotonic() + timeout
|
|
298
|
+
while time.monotonic() < deadline:
|
|
299
|
+
try:
|
|
300
|
+
remaining = deadline - time.monotonic()
|
|
301
|
+
resp = await asyncio.wait_for(self._incoming.get(), timeout=max(remaining, 0.1))
|
|
302
|
+
if resp.get("id") == msg_id:
|
|
303
|
+
if "error" in resp:
|
|
304
|
+
raise RuntimeError(f"RPC error: {resp['error']}")
|
|
305
|
+
return resp
|
|
306
|
+
except asyncio.TimeoutError:
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
raise asyncio.TimeoutError(f"Timeout waiting for response to {method}")
|
|
310
|
+
|
|
311
|
+
async def discover(self) -> dict:
|
|
312
|
+
params = {"from": get_did(), "timestamp": datetime.now(timezone.utc).isoformat()}
|
|
313
|
+
response = await self._send_and_wait("bfp.discover", params)
|
|
314
|
+
return response.get("result", {})
|
|
315
|
+
|
|
316
|
+
async def delegate(self, task: dict) -> str:
|
|
317
|
+
params = {
|
|
318
|
+
"from": get_did(),
|
|
319
|
+
"to": self.remote_did,
|
|
320
|
+
"task": task,
|
|
321
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
322
|
+
}
|
|
323
|
+
response = await self._send_and_wait("bfp.delegate", params)
|
|
324
|
+
result = response.get("result", {})
|
|
325
|
+
return result.get("task_id", "")
|
|
326
|
+
|
|
327
|
+
async def status(self, task_id: str) -> dict:
|
|
328
|
+
params = {
|
|
329
|
+
"from": get_did(),
|
|
330
|
+
"task_id": task_id,
|
|
331
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
332
|
+
}
|
|
333
|
+
response = await self._send_and_wait("bfp.status", params)
|
|
334
|
+
return response.get("result", {})
|
|
335
|
+
|
|
336
|
+
async def cancel(self, task_id: str) -> dict:
|
|
337
|
+
params = {
|
|
338
|
+
"from": get_did(),
|
|
339
|
+
"task_id": task_id,
|
|
340
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
341
|
+
}
|
|
342
|
+
response = await self._send_and_wait("bfp.cancel", params)
|
|
343
|
+
return response.get("result", {})
|
|
344
|
+
|
|
345
|
+
async def ping(self) -> bool:
|
|
346
|
+
params = {"from": get_did(), "timestamp": datetime.now(timezone.utc).isoformat()}
|
|
347
|
+
try:
|
|
348
|
+
response = await self._send_and_wait("bfp.ping", params, timeout=5.0)
|
|
349
|
+
return response.get("result", {}).get("pong", False)
|
|
350
|
+
except (asyncio.TimeoutError, RuntimeError):
|
|
351
|
+
return False
|
|
352
|
+
|
|
353
|
+
async def close(self):
|
|
354
|
+
self._running = False
|
|
355
|
+
if self._reader_task:
|
|
356
|
+
self._reader_task.cancel()
|
|
357
|
+
if self._conn:
|
|
358
|
+
await self._conn.close()
|
|
359
|
+
self._conn = None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
async def send_message(endpoint: str, remote_did: str, method: str, params: dict) -> dict:
|
|
363
|
+
client = BFPClient(endpoint, remote_did)
|
|
364
|
+
try:
|
|
365
|
+
await client.connect()
|
|
366
|
+
return await client._send_and_wait(method, params)
|
|
367
|
+
finally:
|
|
368
|
+
await client.close()
|