runner.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  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 .brief_file import BriefFile
  10. from .control import ControlServer
  11. from .executor import LlmExecutor, NullExecutor
  12. from .llm import BriefEngine, ConversationEngine, LlmClient
  13. from .service import AssistantService
  14. from .tools import default_registry
  15. def build_triage(cfg, client, on_result=None):
  16. """Build (analyzer, lane, brief_fn, brief_reset); (None, None, None, None) when disabled."""
  17. if not cfg.triage.enabled:
  18. return (None, None, None, None)
  19. from .laya_lane import LayaLane
  20. from .triage import TriageAnalyzer
  21. analyzer = TriageAnalyzer(cfg.triage)
  22. lane = LayaLane(
  23. cfg.triage,
  24. on_state=lambda state, reason: analyzer.refresh(state, reason),
  25. on_result=on_result,
  26. )
  27. brief_fn = None
  28. brief_reset = None
  29. if client is not None:
  30. engine = BriefEngine(
  31. cfg.llm,
  32. client,
  33. system_prompt=cfg.llm.triage_system_prompt,
  34. max_tokens=cfg.triage.extract_max_tokens,
  35. timeout_s=cfg.triage.extract_timeout_s,
  36. slot_id=cfg.triage.brief_slot_id,
  37. notify=None,
  38. )
  39. brief_fn = engine.send
  40. brief_reset = engine.reset
  41. return (analyzer, lane, brief_fn, brief_reset)
  42. def main() -> int:
  43. """Entry point for the `stt-assistant` process."""
  44. parser = argparse.ArgumentParser(
  45. description="Assistant controller: subscribe to STT events and serve the control socket"
  46. )
  47. parser.add_argument("--config", type=str, default=None, help="Path to config.toml")
  48. parser.add_argument("-v", "--verbose", action="store_true", help="Enable debug logging")
  49. args = parser.parse_args()
  50. logging.basicConfig(
  51. level=logging.DEBUG if args.verbose else logging.INFO,
  52. format="[%(levelname)s] %(message)s",
  53. )
  54. cfg = load_config(args.config)
  55. subscriber = EventSubscriber(cfg.events.socket or None)
  56. audit = AuditLog(enabled=cfg.audit.enabled, directory=cfg.audit.dir)
  57. executor = NullExecutor()
  58. chat_fn = None
  59. chat_reset = None
  60. session = None
  61. client = None
  62. if cfg.llm.enabled:
  63. client = LlmClient(cfg.llm)
  64. session = ConversationEngine(
  65. cfg.llm,
  66. client,
  67. system_prompt=cfg.llm.system_prompt_chat,
  68. slot_id=cfg.llm.chat_slot_id,
  69. )
  70. executor = LlmExecutor(
  71. cfg.llm,
  72. client,
  73. default_registry(),
  74. slot_id=cfg.llm.command_slot_id,
  75. )
  76. chat_fn = session.send
  77. chat_reset = session.reset
  78. holder = {}
  79. triage, lane, brief_fn, brief_reset = build_triage(
  80. cfg,
  81. client if cfg.llm.enabled else None,
  82. on_result=lambda gen, payload: holder["svc"]._on_triage_result(gen, payload),
  83. )
  84. brief_file = BriefFile(cfg.brief.write)
  85. _debug_factory = None
  86. debug_cfg = getattr(cfg, "debug", None)
  87. if debug_cfg is not None and bool(getattr(debug_cfg, "enabled", False)):
  88. try:
  89. from ..debug import DebugSink
  90. except Exception: # noqa: BLE001 - debug stays disabled without the module
  91. DebugSink = None # type: ignore[assignment]
  92. if DebugSink is not None:
  93. debug_dir = str(getattr(debug_cfg, "dir", "troubleshoot/debug"))
  94. def _make_debug_sink():
  95. return DebugSink(debug_dir, "assistant")
  96. _debug_factory = _make_debug_sink
  97. service = AssistantService(
  98. cfg,
  99. subscriber,
  100. audit=audit,
  101. executor=executor,
  102. chat_fn=chat_fn,
  103. chat_reset=chat_reset,
  104. triage=triage,
  105. laya_lane=lane,
  106. brief_fn=brief_fn,
  107. brief_reset=brief_reset,
  108. brief_file=brief_file,
  109. debug_sink_factory=_debug_factory,
  110. )
  111. holder["svc"] = service
  112. if session is not None:
  113. session.notify = service.chat_notify
  114. control = ControlServer(cfg, service)
  115. service.set_sinks(emit=control.broadcast, relay=control.relay)
  116. control.start()
  117. service.start()
  118. if lane is not None:
  119. lane.start()
  120. logging.info("Assistant ready. Control socket: %s", control.path)
  121. stop = threading.Event()
  122. def handle_signal(_signum, _frame):
  123. stop.set()
  124. signal.signal(signal.SIGINT, handle_signal)
  125. signal.signal(signal.SIGTERM, handle_signal)
  126. stop.wait()
  127. service.stop()
  128. if lane is not None:
  129. lane.stop()
  130. control.close()
  131. return 0