runner.py 4.7 KB

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