runner.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. """Run the assistant process: subscribe to the STT bus and serve the control socket."""
  2. import argparse
  3. import logging
  4. import signal
  5. import threading
  6. from ..bus import EventSubscriber
  7. from ..config import load_config
  8. from .audit import AuditLog
  9. from .control import ControlServer
  10. from .executor import LlmExecutor, NullExecutor
  11. from .llm import ChatSession, LlmClient
  12. from .service import AssistantService
  13. from .tools import default_registry
  14. def build_triage(cfg, client, on_result=None):
  15. """Build (analyzer, lane, extract_fn); (None, None, None) when disabled."""
  16. if not cfg.triage.enabled:
  17. return (None, None, None)
  18. from .laya_lane import LayaLane
  19. from .triage import TriageAnalyzer
  20. analyzer = TriageAnalyzer(cfg.triage)
  21. lane = LayaLane(
  22. cfg.triage,
  23. on_state=lambda state, reason: analyzer.refresh(state, reason),
  24. on_result=on_result,
  25. )
  26. extract = None
  27. if client is not None:
  28. def _extract(prompt):
  29. resp = client.chat(
  30. [
  31. {"role": "system", "content": cfg.llm.triage_system_prompt},
  32. {"role": "user", "content": prompt},
  33. ],
  34. max_tokens=cfg.triage.extract_max_tokens,
  35. timeout_s=cfg.triage.extract_timeout_s,
  36. )
  37. return resp.content
  38. extract = _extract
  39. return (analyzer, lane, extract)
  40. def main() -> int:
  41. """Entry point for the `stt-assistant` process."""
  42. parser = argparse.ArgumentParser(
  43. description="Assistant controller: subscribe to STT events and serve the control socket"
  44. )
  45. parser.add_argument("--config", type=str, default=None, help="Path to config.toml")
  46. parser.add_argument("-v", "--verbose", action="store_true", help="Enable debug logging")
  47. args = parser.parse_args()
  48. logging.basicConfig(
  49. level=logging.DEBUG if args.verbose else logging.INFO,
  50. format="[%(levelname)s] %(message)s",
  51. )
  52. cfg = load_config(args.config)
  53. subscriber = EventSubscriber(cfg.events.socket or None)
  54. audit = AuditLog(enabled=cfg.audit.enabled, directory=cfg.audit.dir)
  55. executor = NullExecutor()
  56. chat_fn = None
  57. chat_reset = None
  58. session = None
  59. client = None
  60. if cfg.llm.enabled:
  61. client = LlmClient(cfg.llm)
  62. session = ChatSession(cfg.llm, client)
  63. executor = LlmExecutor(cfg.llm, client, default_registry())
  64. chat_fn = session.send
  65. chat_reset = session.reset
  66. holder = {}
  67. triage, lane, extract_fn = build_triage(
  68. cfg,
  69. client if cfg.llm.enabled else None,
  70. on_result=lambda gen, payload: holder["svc"]._on_triage_result(gen, payload),
  71. )
  72. service = AssistantService(
  73. cfg,
  74. subscriber,
  75. audit=audit,
  76. executor=executor,
  77. chat_fn=chat_fn,
  78. chat_reset=chat_reset,
  79. triage=triage,
  80. laya_lane=lane,
  81. triage_extract_fn=extract_fn,
  82. )
  83. holder["svc"] = service
  84. if session is not None:
  85. session.notify = service.chat_notify
  86. control = ControlServer(cfg, service)
  87. service.set_sinks(emit=control.broadcast, relay=control.relay)
  88. control.start()
  89. service.start()
  90. if lane is not None:
  91. lane.start()
  92. logging.info("Assistant ready. Control socket: %s", control.path)
  93. stop = threading.Event()
  94. def handle_signal(_signum, _frame):
  95. stop.set()
  96. signal.signal(signal.SIGINT, handle_signal)
  97. signal.signal(signal.SIGTERM, handle_signal)
  98. stop.wait()
  99. service.stop()
  100. if lane is not None:
  101. lane.stop()
  102. control.close()
  103. return 0