@agentunion/kite 1.0.7 → 1.3.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/CHANGELOG.md +208 -0
  2. package/README.md +48 -0
  3. package/cli.js +1 -1
  4. package/extensions/agents/__init__.py +1 -0
  5. package/extensions/agents/assistant/__init__.py +1 -0
  6. package/extensions/agents/assistant/entry.py +329 -0
  7. package/extensions/agents/assistant/module.md +22 -0
  8. package/extensions/agents/assistant/server.py +197 -0
  9. package/extensions/channels/__init__.py +1 -0
  10. package/extensions/channels/acp_channel/__init__.py +1 -0
  11. package/extensions/channels/acp_channel/entry.py +329 -0
  12. package/extensions/channels/acp_channel/module.md +22 -0
  13. package/extensions/channels/acp_channel/server.py +197 -0
  14. package/extensions/event_hub_bench/entry.py +624 -379
  15. package/extensions/event_hub_bench/module.md +2 -1
  16. package/extensions/services/backup/__init__.py +1 -0
  17. package/extensions/services/backup/entry.py +508 -0
  18. package/extensions/services/backup/module.md +22 -0
  19. package/extensions/services/model_service/__init__.py +1 -0
  20. package/extensions/services/model_service/entry.py +508 -0
  21. package/extensions/services/model_service/module.md +22 -0
  22. package/extensions/services/watchdog/entry.py +468 -102
  23. package/extensions/services/watchdog/module.md +3 -0
  24. package/extensions/services/watchdog/monitor.py +170 -69
  25. package/extensions/services/web/__init__.py +1 -0
  26. package/extensions/services/web/config.yaml +149 -0
  27. package/extensions/services/web/entry.py +390 -0
  28. package/extensions/services/web/module.md +24 -0
  29. package/extensions/services/web/routes/__init__.py +1 -0
  30. package/extensions/services/web/routes/routes_call.py +189 -0
  31. package/extensions/services/web/routes/routes_config.py +512 -0
  32. package/extensions/services/web/routes/routes_contacts.py +98 -0
  33. package/extensions/services/web/routes/routes_devlog.py +99 -0
  34. package/extensions/services/web/routes/routes_phone.py +81 -0
  35. package/extensions/services/web/routes/routes_sms.py +48 -0
  36. package/extensions/services/web/routes/routes_stats.py +17 -0
  37. package/extensions/services/web/routes/routes_voicechat.py +554 -0
  38. package/extensions/services/web/routes/schemas.py +216 -0
  39. package/extensions/services/web/server.py +375 -0
  40. package/extensions/services/web/static/css/style.css +1064 -0
  41. package/extensions/services/web/static/index.html +1445 -0
  42. package/extensions/services/web/static/js/app.js +4671 -0
  43. package/extensions/services/web/vendor/__init__.py +1 -0
  44. package/extensions/services/web/vendor/bluetooth/audio.py +348 -0
  45. package/extensions/services/web/vendor/bluetooth/contacts.py +251 -0
  46. package/extensions/services/web/vendor/bluetooth/manager.py +395 -0
  47. package/extensions/services/web/vendor/bluetooth/sms.py +290 -0
  48. package/extensions/services/web/vendor/bluetooth/telephony.py +274 -0
  49. package/extensions/services/web/vendor/config.py +139 -0
  50. package/extensions/services/web/vendor/conversation/asr.py +936 -0
  51. package/extensions/services/web/vendor/conversation/engine.py +548 -0
  52. package/extensions/services/web/vendor/conversation/llm.py +534 -0
  53. package/extensions/services/web/vendor/conversation/mcp_tools.py +190 -0
  54. package/extensions/services/web/vendor/conversation/tts.py +322 -0
  55. package/extensions/services/web/vendor/conversation/vad.py +138 -0
  56. package/extensions/services/web/vendor/storage/__init__.py +1 -0
  57. package/extensions/services/web/vendor/storage/identity.py +312 -0
  58. package/extensions/services/web/vendor/storage/store.py +507 -0
  59. package/extensions/services/web/vendor/task/manager.py +864 -0
  60. package/extensions/services/web/vendor/task/models.py +45 -0
  61. package/extensions/services/web/vendor/task/webhook.py +263 -0
  62. package/extensions/services/web/vendor/tools/registry.py +321 -0
  63. package/kernel/__init__.py +0 -0
  64. package/kernel/entry.py +407 -0
  65. package/{core/event_hub/hub.py → kernel/event_hub.py} +62 -74
  66. package/kernel/module.md +33 -0
  67. package/{core/registry/store.py → kernel/registry_store.py} +23 -8
  68. package/kernel/rpc_router.py +388 -0
  69. package/kernel/server.py +267 -0
  70. package/launcher/__init__.py +10 -0
  71. package/launcher/__main__.py +6 -0
  72. package/launcher/count_lines.py +258 -0
  73. package/launcher/entry.py +1778 -0
  74. package/launcher/logging_setup.py +289 -0
  75. package/{core/launcher → launcher}/module_scanner.py +11 -6
  76. package/launcher/process_manager.py +880 -0
  77. package/main.py +11 -210
  78. package/package.json +6 -9
  79. package/__init__.py +0 -1
  80. package/__main__.py +0 -15
  81. package/core/event_hub/BENCHMARK.md +0 -94
  82. package/core/event_hub/bench.py +0 -459
  83. package/core/event_hub/bench_extreme.py +0 -308
  84. package/core/event_hub/bench_perf.py +0 -350
  85. package/core/event_hub/entry.py +0 -157
  86. package/core/event_hub/module.md +0 -20
  87. package/core/event_hub/server.py +0 -206
  88. package/core/launcher/entry.py +0 -1158
  89. package/core/launcher/process_manager.py +0 -470
  90. package/core/registry/entry.py +0 -110
  91. package/core/registry/module.md +0 -30
  92. package/core/registry/server.py +0 -289
  93. package/extensions/services/watchdog/server.py +0 -167
  94. /package/{core → extensions/services/web/vendor/bluetooth}/__init__.py +0 -0
  95. /package/{core/event_hub → extensions/services/web/vendor/conversation}/__init__.py +0 -0
  96. /package/{core/launcher → extensions/services/web/vendor/task}/__init__.py +0 -0
  97. /package/{core/registry → extensions/services/web/vendor/tools}/__init__.py +0 -0
  98. /package/{core/event_hub → kernel}/dedup.py +0 -0
  99. /package/{core/event_hub → kernel}/router.py +0 -0
  100. /package/{core/launcher → launcher}/module.md +0 -0
@@ -0,0 +1,508 @@
1
+ """
2
+ Model Service entry point.
3
+ Reads boot_info from stdin, registers to Registry, starts model_service service.
4
+ """
5
+
6
+ import builtins
7
+ import json
8
+ import os
9
+ import sys
10
+ import threading
11
+ import re
12
+ import time
13
+ from datetime import datetime, timezone
14
+
15
+ import asyncio
16
+ import traceback
17
+ import uuid
18
+
19
+ import websockets
20
+
21
+
22
+ # ── Safe stdout/stderr: ignore BrokenPipeError after Launcher closes stdio ──
23
+
24
+
25
+ # ── Module configuration ──
26
+ MODULE_NAME = "model_service"
27
+
28
+
29
+ class _SafeWriter:
30
+ """Wraps a stream to silently swallow BrokenPipeError on write/flush."""
31
+ def __init__(self, stream):
32
+ self._stream = stream
33
+
34
+ def write(self, s):
35
+ try:
36
+ self._stream.write(s)
37
+ except (BrokenPipeError, OSError):
38
+ pass
39
+
40
+ def flush(self):
41
+ try:
42
+ self._stream.flush()
43
+ except (BrokenPipeError, OSError):
44
+ pass
45
+
46
+ def __getattr__(self, name):
47
+ return getattr(self._stream, name)
48
+
49
+ sys.stdout = _SafeWriter(sys.stdout)
50
+ sys.stderr = _SafeWriter(sys.stderr)
51
+
52
+
53
+ # ── Timestamped print + log file writer ──
54
+ # Independent implementation per module (no shared code dependency)
55
+
56
+ _builtin_print = builtins.print
57
+ _start_ts = time.monotonic()
58
+ _last_ts = time.monotonic()
59
+ _ANSI_RE = re.compile(r"\033\[[0-9;]*m")
60
+ _log_lock = threading.Lock()
61
+ _log_latest_path = None
62
+ _log_daily_path = None
63
+ _log_daily_date = ""
64
+ _log_dir = None
65
+ _crash_log_path = None
66
+
67
+ def _strip_ansi(s: str) -> str:
68
+ return _ANSI_RE.sub("", s)
69
+
70
+ def _resolve_daily_log_path():
71
+ """Resolve daily log path based on current date."""
72
+ global _log_daily_path, _log_daily_date
73
+ if not _log_dir:
74
+ return
75
+ today = datetime.now().strftime("%Y-%m-%d")
76
+ if today == _log_daily_date and _log_daily_path:
77
+ return
78
+ month_dir = os.path.join(_log_dir, today[:7])
79
+ os.makedirs(month_dir, exist_ok=True)
80
+ _log_daily_path = os.path.join(month_dir, f"{today}.log")
81
+ _log_daily_date = today
82
+
83
+ def _write_log(plain_line: str):
84
+ """Write a plain-text line to both latest.log and daily log."""
85
+ with _log_lock:
86
+ if _log_latest_path:
87
+ try:
88
+ with open(_log_latest_path, "a", encoding="utf-8") as f:
89
+ f.write(plain_line)
90
+ except Exception:
91
+ pass
92
+ _resolve_daily_log_path()
93
+ if _log_daily_path:
94
+ try:
95
+ with open(_log_daily_path, "a", encoding="utf-8") as f:
96
+ f.write(plain_line)
97
+ except Exception:
98
+ pass
99
+
100
+ def _write_crash(exc_type, exc_value, exc_tb, thread_name=None, severity="critical", handled=False):
101
+ """Write crash record to crashes.jsonl + daily crash archive."""
102
+ record = {
103
+ "timestamp": datetime.now(timezone.utc).isoformat(),
104
+ "module": MODULE_NAME,
105
+ "thread": thread_name or threading.current_thread().name,
106
+ "exception_type": exc_type.__name__ if exc_type else "Unknown",
107
+ "exception_message": str(exc_value),
108
+ "traceback": "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
109
+ "severity": severity,
110
+ "handled": handled,
111
+ "process_id": os.getpid(),
112
+ "platform": sys.platform,
113
+ "runtime_version": f"Python {sys.version.split()[0]}",
114
+ }
115
+
116
+ if exc_tb:
117
+ tb_entries = traceback.extract_tb(exc_tb)
118
+ if tb_entries:
119
+ last = tb_entries[-1]
120
+ record["context"] = {
121
+ "function": last.name,
122
+ "file": os.path.basename(last.filename),
123
+ "line": last.lineno,
124
+ }
125
+
126
+ line = json.dumps(record, ensure_ascii=False) + "\n"
127
+
128
+ if _crash_log_path:
129
+ try:
130
+ with open(_crash_log_path, "a", encoding="utf-8") as f:
131
+ f.write(line)
132
+ except Exception:
133
+ pass
134
+
135
+ if _log_dir:
136
+ try:
137
+ today = datetime.now().strftime("%Y-%m-%d")
138
+ archive_dir = os.path.join(_log_dir, "crashes", today[:7])
139
+ os.makedirs(archive_dir, exist_ok=True)
140
+ archive_path = os.path.join(archive_dir, f"{today}.jsonl")
141
+ with open(archive_path, "a", encoding="utf-8") as f:
142
+ f.write(line)
143
+ except Exception:
144
+ pass
145
+
146
+ def _print_crash_summary(exc_type, exc_tb, thread_name=None):
147
+ """Print crash summary to console (red highlight)."""
148
+ RED = "\033[91m"
149
+ RESET = "\033[0m"
150
+
151
+ if exc_tb:
152
+ tb_entries = traceback.extract_tb(exc_tb)
153
+ if tb_entries:
154
+ last = tb_entries[-1]
155
+ location = f"{os.path.basename(last.filename)}:{last.lineno}"
156
+ else:
157
+ location = "unknown"
158
+ else:
159
+ location = "unknown"
160
+
161
+ prefix = f"[{MODULE_NAME}]"
162
+ if thread_name:
163
+ _builtin_print(f"{prefix} {RED}线程 {thread_name} 崩溃: "
164
+ f"{exc_type.__name__} in {location}{RESET}")
165
+ else:
166
+ _builtin_print(f"{prefix} {RED}崩溃: {exc_type.__name__} in {location}{RESET}")
167
+ if _crash_log_path:
168
+ _builtin_print(f"{prefix} 崩溃日志: {_crash_log_path}")
169
+
170
+ def _setup_exception_hooks():
171
+ """Set up global exception hooks."""
172
+ _orig_excepthook = sys.excepthook
173
+
174
+ def _excepthook(exc_type, exc_value, exc_tb):
175
+ _write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=False)
176
+ _print_crash_summary(exc_type, exc_tb)
177
+ _orig_excepthook(exc_type, exc_value, exc_tb)
178
+
179
+ sys.excepthook = _excepthook
180
+
181
+ if hasattr(threading, "excepthook"):
182
+ def _thread_excepthook(args):
183
+ _write_crash(args.exc_type, args.exc_value, args.exc_traceback,
184
+ thread_name=args.thread.name if args.thread else "unknown",
185
+ severity="error", handled=False)
186
+ _print_crash_summary(args.exc_type, args.exc_traceback,
187
+ thread_name=args.thread.name if args.thread else None)
188
+
189
+ threading.excepthook = _thread_excepthook
190
+
191
+ def _tprint(*args, **kwargs):
192
+ """Timestamped print that adds [timestamp] HH:MM:SS.mmm +delta prefix."""
193
+ global _last_ts
194
+ now = time.monotonic()
195
+ elapsed = now - _start_ts
196
+ delta = now - _last_ts
197
+ _last_ts = now
198
+
199
+ if elapsed < 1:
200
+ elapsed_str = f"{elapsed * 1000:.0f}ms"
201
+ elif elapsed < 100:
202
+ elapsed_str = f"{elapsed:.1f}s"
203
+ else:
204
+ elapsed_str = f"{elapsed:.0f}s"
205
+
206
+ if delta < 0.001:
207
+ delta_str = ""
208
+ elif delta < 1:
209
+ delta_str = f"+{delta * 1000:.0f}ms"
210
+ elif delta < 100:
211
+ delta_str = f"+{delta:.1f}s"
212
+ else:
213
+ delta_str = f"+{delta:.0f}s"
214
+
215
+ ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
216
+
217
+ _builtin_print(*args, **kwargs)
218
+
219
+ if _log_latest_path or _log_daily_path:
220
+ sep = kwargs.get("sep", " ")
221
+ end = kwargs.get("end", "\n")
222
+ text = sep.join(str(a) for a in args)
223
+ prefix = f"[{elapsed_str:>6}] {ts} {delta_str:>8} "
224
+ _write_log(prefix + _strip_ansi(text) + end)
225
+
226
+ builtins.print = _tprint
227
+
228
+ # Ensure project root is on sys.path (set by main.py or cli.js)
229
+ _project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
230
+ if _project_root not in sys.path:
231
+ sys.path.insert(0, _project_root)
232
+
233
+
234
+
235
+ def _fmt_elapsed(t0: float) -> str:
236
+ d = time.monotonic() - t0
237
+ if d < 1:
238
+ return f"{d * 1000:.0f}ms"
239
+ if d < 10:
240
+ return f"{d:.1f}s"
241
+ return f"{d:.0f}s"
242
+
243
+
244
+ def _read_stdin_kite_message(expected_type: str, timeout: float = 10) -> dict | None:
245
+ """Read a single kite message of expected type from stdin with timeout."""
246
+ result = [None]
247
+
248
+ def _read():
249
+ try:
250
+ line = sys.stdin.readline().strip()
251
+ if line:
252
+ msg = json.loads(line)
253
+ if isinstance(msg, dict) and msg.get("kite") == expected_type:
254
+ result[0] = msg
255
+ except Exception:
256
+ pass
257
+
258
+ t = threading.Thread(target=_read, daemon=True)
259
+ t.start()
260
+ t.join(timeout=timeout)
261
+ return result[0]
262
+
263
+
264
+ # Global WS reference for publish_event callback
265
+ _ws_global = None
266
+
267
+
268
+ async def main():
269
+ global _ws_global
270
+ # Initialize log file paths
271
+ global _log_dir, _log_latest_path, _crash_log_path
272
+ module_data = os.environ.get("KITE_MODULE_DATA")
273
+ if module_data:
274
+ _log_dir = os.path.join(module_data, "log")
275
+ os.makedirs(_log_dir, exist_ok=True)
276
+ suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
277
+
278
+ _log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
279
+ try:
280
+ with open(_log_latest_path, "w", encoding="utf-8") as f:
281
+ pass
282
+ except Exception:
283
+ _log_latest_path = None
284
+
285
+ _crash_log_path = os.path.join(_log_dir, f"crashes{suffix}.jsonl")
286
+ try:
287
+ with open(_crash_log_path, "w", encoding="utf-8") as f:
288
+ pass
289
+ except Exception:
290
+ _crash_log_path = None
291
+
292
+ _resolve_daily_log_path()
293
+
294
+ _setup_exception_hooks()
295
+
296
+ _t0 = time.monotonic()
297
+
298
+ # Read boot_info from stdin (only token)
299
+ token = ""
300
+ try:
301
+ line = sys.stdin.readline().strip()
302
+ if line:
303
+ boot_info = json.loads(line)
304
+ token = boot_info.get("token", "")
305
+ except Exception:
306
+ pass
307
+
308
+ # Read kernel_port: env first (fast path), stdin fallback (parallel start)
309
+ kernel_port = int(os.environ.get("KITE_KERNEL_PORT", "0"))
310
+ if not kernel_port:
311
+ msg = _read_stdin_kite_message("kernel_port", timeout=10)
312
+ if msg:
313
+ kernel_port = int(msg.get("kernel_port", 0))
314
+
315
+ if not token or not kernel_port:
316
+ print("[model_service] ERROR: Missing token or kernel_port")
317
+ sys.exit(1)
318
+
319
+ print(f"[model_service] Token received ({len(token)} chars), kernel port: {kernel_port} ({_fmt_elapsed(_t0)})")
320
+
321
+ # Connect to Kernel WebSocket
322
+ ws_url = f"ws://127.0.0.1:{kernel_port}/ws?token={token}&id=model_service"
323
+ print(f"[model_service] Connecting to Kernel: {ws_url}")
324
+
325
+ try:
326
+ async with websockets.connect(ws_url, open_timeout=5, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
327
+ _ws_global = ws
328
+ print(f"[model_service] Connected to Kernel ({_fmt_elapsed(_t0)})")
329
+
330
+ # Subscribe to events
331
+ await _rpc_call(ws, "event.subscribe", {
332
+ "events": [
333
+ "module.started",
334
+ "module.stopped",
335
+ "module.shutdown",
336
+ ],
337
+ })
338
+ print(f"[model_service] Subscribed to events ({_fmt_elapsed(_t0)})")
339
+
340
+ # Register to Kernel Registry via RPC
341
+ await _rpc_call(ws, "registry.register", {
342
+ "module_id": "model_service",
343
+ "module_type": "service",
344
+ "events_publish": {
345
+ "model_service.test": {"description": "Test event from model_service module"},
346
+ },
347
+ "events_subscribe": [
348
+ "module.started",
349
+ "module.stopped",
350
+ "module.shutdown",
351
+ ],
352
+ })
353
+ print(f"[model_service] Registered to Kernel ({_fmt_elapsed(_t0)})")
354
+
355
+ # Publish module.ready
356
+ await _rpc_call(ws, "event.publish", {
357
+ "event_id": str(uuid.uuid4()),
358
+ "event": "module.ready",
359
+ "data": {
360
+ "module_id": "model_service",
361
+ "graceful_shutdown": True,
362
+ },
363
+ })
364
+ print(f"[model_service] module.ready published ({_fmt_elapsed(_t0)})")
365
+
366
+ # Start test event loop in background
367
+ test_task = asyncio.create_task(_test_event_loop(ws))
368
+
369
+ # Message loop: handle incoming RPC + events
370
+ async for raw in ws:
371
+ try:
372
+ msg = json.loads(raw)
373
+ except (json.JSONDecodeError, TypeError):
374
+ continue
375
+
376
+ try:
377
+ has_method = "method" in msg
378
+ has_id = "id" in msg
379
+
380
+ if has_method and not has_id:
381
+ # Event Notification
382
+ await _handle_event_notification(msg)
383
+ elif has_method and has_id:
384
+ # Incoming RPC request
385
+ await _handle_rpc_request(ws, msg)
386
+ # Ignore RPC responses (we don't await them in this simple impl)
387
+ except Exception as e:
388
+ print(f"[model_service] 消息处理异常(已忽略): {e}")
389
+
390
+ except Exception as e:
391
+ _write_crash(type(e), e, e.__traceback__, severity="critical", handled=True)
392
+ _print_crash_summary(type(e), e.__traceback__)
393
+ sys.exit(1)
394
+
395
+
396
+ async def _rpc_call(ws, method: str, params: dict = None):
397
+ """Send a JSON-RPC 2.0 request (fire-and-forget, no response awaited)."""
398
+ msg = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method}
399
+ if params:
400
+ msg["params"] = params
401
+ await ws.send(json.dumps(msg))
402
+
403
+
404
+ async def _publish_event(ws, event: dict):
405
+ """Publish an event via RPC event.publish."""
406
+ await _rpc_call(ws, "event.publish", {
407
+ "event_id": str(uuid.uuid4()),
408
+ "event": event.get("event", ""),
409
+ "data": event.get("data", {}),
410
+ })
411
+
412
+
413
+ async def _handle_event_notification(msg: dict):
414
+ """Handle an event notification (JSON-RPC 2.0 Notification with method='event')."""
415
+ params = msg.get("params", {})
416
+ event_type = params.get("event", "")
417
+ data = params.get("data", {})
418
+
419
+ # Special handling for module.shutdown targeting model_service
420
+ if event_type == "module.shutdown" and data.get("module_id") == "model_service":
421
+ await _handle_shutdown()
422
+ return
423
+
424
+ # Log other events
425
+ print(f"[model_service] Event received: {event_type}")
426
+
427
+
428
+ async def _handle_rpc_request(ws, msg: dict):
429
+ """Handle an incoming RPC request (model_service.* methods)."""
430
+ rpc_id = msg.get("id", "")
431
+ method = msg.get("method", "")
432
+ params = msg.get("params", {})
433
+
434
+ handlers = {
435
+ "health": lambda p: _rpc_health(),
436
+ "status": lambda p: _rpc_status(),
437
+ }
438
+ handler = handlers.get(method)
439
+ if handler:
440
+ try:
441
+ result = await handler(params)
442
+ await ws.send(json.dumps({"jsonrpc": "2.0", "id": rpc_id, "result": result}))
443
+ except Exception as e:
444
+ await ws.send(json.dumps({
445
+ "jsonrpc": "2.0", "id": rpc_id,
446
+ "error": {"code": -32603, "message": str(e)},
447
+ }))
448
+ else:
449
+ await ws.send(json.dumps({
450
+ "jsonrpc": "2.0", "id": rpc_id,
451
+ "error": {"code": -32601, "message": f"Method not found: {method}"},
452
+ }))
453
+
454
+
455
+ async def _rpc_health() -> dict:
456
+ """RPC handler for model_service.health."""
457
+ return {
458
+ "status": "healthy",
459
+ "details": {
460
+ "uptime_seconds": round(time.time() - _start_ts),
461
+ },
462
+ }
463
+
464
+
465
+ async def _rpc_status() -> dict:
466
+ """RPC handler for model_service.status."""
467
+ return {
468
+ "module": "model_service",
469
+ "status": "running",
470
+ "uptime_seconds": round(time.time() - _start_ts),
471
+ }
472
+
473
+
474
+ async def _handle_shutdown():
475
+ """Handle module.shutdown event — ack, cleanup, ready, exit."""
476
+ print("[model_service] Received shutdown request")
477
+ # Step 1: Send ack
478
+ await _publish_event(_ws_global, {
479
+ "event": "module.shutdown.ack",
480
+ "data": {"module_id": "model_service", "estimated_cleanup": 2},
481
+ })
482
+ # Step 2: Cleanup (nothing to clean up for model_service)
483
+ # Step 3: Send ready
484
+ await _publish_event(_ws_global, {
485
+ "event": "module.shutdown.ready",
486
+ "data": {"module_id": "model_service"},
487
+ })
488
+ print("[model_service] Shutdown ready, exiting")
489
+ # Step 4: Exit
490
+ sys.exit(0)
491
+
492
+
493
+ async def _test_event_loop(ws):
494
+ """Publish a test event every 10 seconds."""
495
+ while True:
496
+ await asyncio.sleep(10)
497
+ await _publish_event(ws, {
498
+ "event": "model_service.test",
499
+ "data": {
500
+ "message": "test event from model_service",
501
+ "timestamp": datetime.now(timezone.utc).isoformat(),
502
+ },
503
+ })
504
+ print("[model_service] test event published")
505
+
506
+
507
+ if __name__ == "__main__":
508
+ asyncio.run(main())
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: model_service
3
+ display_name: Model Service
4
+ version: "1.0"
5
+ type: service
6
+ state: enabled
7
+ runtime: python
8
+ entry: entry.py
9
+ events:
10
+ - model_service.test
11
+ subscriptions:
12
+ - module.started
13
+ - module.stopped
14
+ - module.shutdown
15
+ ---
16
+
17
+ # Model Service(大模型服务)
18
+
19
+ 大模型服务模块,提供统一的 LLM 调用接口。
20
+
21
+ - 模型调用 — 封装多种大模型 API 的统一调用接口
22
+ - 事件通知 — 通过 Event Hub 发布模型服务状态事件