@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.
Files changed (100) hide show
  1. package/README.md +90 -0
  2. package/bin/bmo.js +188 -0
  3. package/cli.py +1129 -0
  4. package/config/__init__.py +0 -0
  5. package/config/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/config/__pycache__/settings.cpython-313.pyc +0 -0
  7. package/config/__pycache__/system-prompt.cpython-313.pyc +0 -0
  8. package/config/settings.py +104 -0
  9. package/config/system-prompt.json +18 -0
  10. package/core/__init__.py +0 -0
  11. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  12. package/core/__pycache__/bfp_a2a_bridge.cpython-313.pyc +0 -0
  13. package/core/__pycache__/bfp_agent.cpython-313.pyc +0 -0
  14. package/core/__pycache__/bfp_agent_card.cpython-313.pyc +0 -0
  15. package/core/__pycache__/bfp_connector.cpython-313.pyc +0 -0
  16. package/core/__pycache__/bfp_discovery.cpython-313.pyc +0 -0
  17. package/core/__pycache__/bfp_identity.cpython-313.pyc +0 -0
  18. package/core/__pycache__/bfp_tasks.cpython-313.pyc +0 -0
  19. package/core/__pycache__/bfp_transport.cpython-313.pyc +0 -0
  20. package/core/__pycache__/bmo_engine.cpython-313.pyc +0 -0
  21. package/core/__pycache__/bot_client.cpython-313.pyc +0 -0
  22. package/core/__pycache__/budget_tracker.cpython-313.pyc +0 -0
  23. package/core/__pycache__/cli_renderer.cpython-313.pyc +0 -0
  24. package/core/__pycache__/goal_runner.cpython-313.pyc +0 -0
  25. package/core/__pycache__/request_worker.cpython-313.pyc +0 -0
  26. package/core/__pycache__/security.cpython-313.pyc +0 -0
  27. package/core/__pycache__/shared_state.cpython-313.pyc +0 -0
  28. package/core/__pycache__/worker_manager.cpython-313.pyc +0 -0
  29. package/core/__pycache__/worker_multiproc.cpython-313.pyc +0 -0
  30. package/core/__pycache__/worker_protocol.cpython-313.pyc +0 -0
  31. package/core/__pycache__/worker_subprocess.cpython-313.pyc +0 -0
  32. package/core/bfp_a2a_bridge.py +399 -0
  33. package/core/bfp_agent.py +98 -0
  34. package/core/bfp_agent_card.py +161 -0
  35. package/core/bfp_connector.py +177 -0
  36. package/core/bfp_discovery.py +105 -0
  37. package/core/bfp_identity.py +83 -0
  38. package/core/bfp_tasks.py +70 -0
  39. package/core/bfp_transport.py +368 -0
  40. package/core/bmo_engine.py +405 -0
  41. package/core/bot_client.py +838 -0
  42. package/core/budget_tracker.py +62 -0
  43. package/core/cli_renderer.py +177 -0
  44. package/core/goal_runner.py +129 -0
  45. package/core/request_worker.py +242 -0
  46. package/core/security.py +42 -0
  47. package/core/shared_state.py +4 -0
  48. package/core/worker_manager.py +71 -0
  49. package/core/worker_multiproc.py +155 -0
  50. package/core/worker_protocol.py +30 -0
  51. package/core/worker_subprocess.py +222 -0
  52. package/handlers/__init__.py +0 -0
  53. package/handlers/__pycache__/__init__.cpython-313.pyc +0 -0
  54. package/handlers/__pycache__/messages.cpython-313.pyc +0 -0
  55. package/handlers/messages.py +2761 -0
  56. package/main.py +125 -0
  57. package/memory.md +43 -0
  58. package/models/__init__.py +0 -0
  59. package/models/__pycache__/__init__.cpython-313.pyc +0 -0
  60. package/models/__pycache__/chat_models.cpython-313.pyc +0 -0
  61. package/models/chat_models.py +143 -0
  62. package/package.json +50 -0
  63. package/registry/worker.js +108 -0
  64. package/registry/wrangler.toml +11 -0
  65. package/requirements.txt +13 -0
  66. package/scripts/bmo_init.js +115 -0
  67. package/scripts/postinstall.js +265 -0
  68. package/scripts/relay_cmd.js +276 -0
  69. package/scripts/web_cmd.js +136 -0
  70. package/setup.py +26 -0
  71. package/storage/__init__.py +0 -0
  72. package/storage/__pycache__/__init__.cpython-313.pyc +0 -0
  73. package/storage/__pycache__/sqlite_storage.cpython-313.pyc +0 -0
  74. package/storage/__pycache__/storage.cpython-313.pyc +0 -0
  75. package/storage/sqlite_storage.py +658 -0
  76. package/storage/storage.py +265 -0
  77. package/tools/__pycache__/bfp_relay.cpython-313.pyc +0 -0
  78. package/tools/__pycache__/get_session_summaries.cpython-313.pyc +0 -0
  79. package/tools/__pycache__/mcp_bridge.cpython-313.pyc +0 -0
  80. package/tools/__pycache__/mcp_server.cpython-313.pyc +0 -0
  81. package/tools/__pycache__/run_mcp_standalone.cpython-313.pyc +0 -0
  82. package/tools/__pycache__/task_registry.cpython-313.pyc +0 -0
  83. package/tools/__pycache__/test_mcp_connection.cpython-313.pyc +0 -0
  84. package/tools/bfp_relay.py +359 -0
  85. package/tools/bot.db +0 -0
  86. package/tools/get_session_summaries.py +45 -0
  87. package/tools/mcp_bridge.py +109 -0
  88. package/tools/mcp_server.py +531 -0
  89. package/tools/register_mcp_task.py +20 -0
  90. package/tools/run_detached.bat +32 -0
  91. package/tools/run_mcp_standalone.py +16 -0
  92. package/tools/task_registry.py +184 -0
  93. package/tools/test_mcp_connection.py +80 -0
  94. package/webchat/package-lock.json +1528 -0
  95. package/webchat/package.json +12 -0
  96. package/webchat/public/app.js +1293 -0
  97. package/webchat/public/index.html +226 -0
  98. package/webchat/public/index.html.bak +416 -0
  99. package/webchat/public/styles.css +2435 -0
  100. 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()