feat: polish life os cards and voice stack
This commit is contained in:
parent
66362c7176
commit
0edf8c3fef
21 changed files with 3681 additions and 502 deletions
|
|
@ -28,10 +28,11 @@ nanobot -> client notifications::
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, Awaitable, Callable
|
||||
|
||||
from wisper import WisperBus, WisperEvent
|
||||
|
||||
|
|
@ -56,13 +57,21 @@ def _jsonrpc_notification(method: str, params: dict[str, Any] | None = None) ->
|
|||
class NanobotApiProcess:
|
||||
"""Connects to the running nanobot process via its Unix domain socket."""
|
||||
|
||||
def __init__(self, bus: WisperBus, socket_path: Path) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
bus: WisperBus,
|
||||
socket_path: Path,
|
||||
on_disconnect: Callable[[], Awaitable[None]] | None = None,
|
||||
) -> None:
|
||||
self._bus = bus
|
||||
self._socket_path = socket_path
|
||||
self._on_disconnect = on_disconnect
|
||||
self._reader: asyncio.StreamReader | None = None
|
||||
self._writer: asyncio.StreamWriter | None = None
|
||||
self._read_task: asyncio.Task | None = None
|
||||
self._socket_inode: int | None = None
|
||||
self._streaming_partial_response = False
|
||||
self._closing = False
|
||||
|
||||
@property
|
||||
def running(self) -> bool:
|
||||
|
|
@ -88,6 +97,8 @@ class NanobotApiProcess:
|
|||
await self._bus.publish(WisperEvent(role="system", text="Already connected to nanobot."))
|
||||
return
|
||||
|
||||
self._closing = False
|
||||
self._streaming_partial_response = False
|
||||
if not self._socket_path.exists():
|
||||
await self._bus.publish(
|
||||
WisperEvent(
|
||||
|
|
@ -172,6 +183,7 @@ class NanobotApiProcess:
|
|||
await self._bus.publish(WisperEvent(role="system", text="Disconnected from nanobot."))
|
||||
|
||||
async def _cleanup(self) -> None:
|
||||
self._closing = True
|
||||
if self._read_task and not self._read_task.done():
|
||||
self._read_task.cancel()
|
||||
try:
|
||||
|
|
@ -189,6 +201,7 @@ class NanobotApiProcess:
|
|||
self._writer = None
|
||||
self._reader = None
|
||||
self._socket_inode = None
|
||||
self._streaming_partial_response = False
|
||||
|
||||
async def _send_notification(self, method: str, params: dict[str, Any]) -> None:
|
||||
assert self._writer is not None
|
||||
|
|
@ -207,9 +220,19 @@ class NanobotApiProcess:
|
|||
break
|
||||
await self._handle_line(line)
|
||||
finally:
|
||||
await self._bus.publish(WisperEvent(role="system", text="Nanobot closed the connection."))
|
||||
should_notify_disconnect = not self._closing
|
||||
self._streaming_partial_response = False
|
||||
self._writer = None
|
||||
self._reader = None
|
||||
if should_notify_disconnect:
|
||||
await self._bus.publish(
|
||||
WisperEvent(role="system", text="Nanobot closed the connection.")
|
||||
)
|
||||
if self._on_disconnect is not None:
|
||||
asyncio.create_task(
|
||||
self._on_disconnect(),
|
||||
name="nanobot-api-reconnect-trigger",
|
||||
)
|
||||
|
||||
async def _handle_line(self, line: bytes) -> None:
|
||||
raw = line.decode(errors="replace").strip()
|
||||
|
|
@ -245,12 +268,21 @@ class NanobotApiProcess:
|
|||
content = str(params.get("content", ""))
|
||||
is_progress = bool(params.get("is_progress", False))
|
||||
is_tool_hint = bool(params.get("is_tool_hint", False))
|
||||
is_partial = bool(params.get("is_partial", False))
|
||||
if is_progress:
|
||||
if is_partial:
|
||||
self._streaming_partial_response = True
|
||||
await self._bus.publish(WisperEvent(role="nanobot-tts-partial", text=content))
|
||||
return
|
||||
role = "nanobot-tool" if is_tool_hint else "nanobot-progress"
|
||||
await self._bus.publish(WisperEvent(role=role, text=content))
|
||||
else:
|
||||
await self._bus.publish(WisperEvent(role="nanobot", text=content))
|
||||
await self._bus.publish(WisperEvent(role="nanobot-tts", text=content))
|
||||
if self._streaming_partial_response:
|
||||
self._streaming_partial_response = False
|
||||
await self._bus.publish(WisperEvent(role="nanobot-tts-flush", text=""))
|
||||
else:
|
||||
await self._bus.publish(WisperEvent(role="nanobot-tts", text=content))
|
||||
elif method == "agent_state":
|
||||
state = str(params.get("state", ""))
|
||||
await self._bus.publish(WisperEvent(role="agent-state", text=state))
|
||||
|
|
@ -263,9 +295,52 @@ class SuperTonicGateway:
|
|||
self.bus = WisperBus()
|
||||
self._lock = asyncio.Lock()
|
||||
self._process: NanobotApiProcess | None = None
|
||||
self._reconnect_task: asyncio.Task[None] | None = None
|
||||
self._shutdown = False
|
||||
socket_path = Path(os.getenv("NANOBOT_API_SOCKET", str(DEFAULT_SOCKET_PATH))).expanduser()
|
||||
self._socket_path = socket_path
|
||||
|
||||
def _new_process(self) -> NanobotApiProcess:
|
||||
return NanobotApiProcess(
|
||||
bus=self.bus,
|
||||
socket_path=self._socket_path,
|
||||
on_disconnect=self._schedule_reconnect,
|
||||
)
|
||||
|
||||
async def _schedule_reconnect(self) -> None:
|
||||
async with self._lock:
|
||||
if self._shutdown:
|
||||
return
|
||||
if self._process and self._process.running:
|
||||
return
|
||||
if self._reconnect_task and not self._reconnect_task.done():
|
||||
return
|
||||
self._reconnect_task = asyncio.create_task(
|
||||
self._reconnect_loop(),
|
||||
name="nanobot-api-reconnect",
|
||||
)
|
||||
|
||||
async def _reconnect_loop(self) -> None:
|
||||
delay_s = 0.5
|
||||
try:
|
||||
while not self._shutdown:
|
||||
async with self._lock:
|
||||
if self._process and self._process.running:
|
||||
return
|
||||
self._process = self._new_process()
|
||||
await self._process.start()
|
||||
if self._process.running:
|
||||
return
|
||||
await asyncio.sleep(delay_s)
|
||||
delay_s = min(delay_s * 2.0, 5.0)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
finally:
|
||||
async with self._lock:
|
||||
current_task = asyncio.current_task()
|
||||
if self._reconnect_task is current_task:
|
||||
self._reconnect_task = None
|
||||
|
||||
async def subscribe(self) -> asyncio.Queue[WisperEvent]:
|
||||
return await self.bus.subscribe()
|
||||
|
||||
|
|
@ -274,10 +349,16 @@ class SuperTonicGateway:
|
|||
|
||||
async def connect_nanobot(self) -> None:
|
||||
async with self._lock:
|
||||
self._shutdown = False
|
||||
if self._process and self._process.running:
|
||||
await self.bus.publish(WisperEvent(role="system", text="Already connected to nanobot."))
|
||||
return
|
||||
self._process = NanobotApiProcess(bus=self.bus, socket_path=self._socket_path)
|
||||
if self._reconnect_task and not self._reconnect_task.done():
|
||||
self._reconnect_task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await self._reconnect_task
|
||||
self._reconnect_task = None
|
||||
self._process = self._new_process()
|
||||
await self._process.start()
|
||||
|
||||
async def _ensure_connected_process(self) -> NanobotApiProcess:
|
||||
|
|
@ -285,7 +366,12 @@ class SuperTonicGateway:
|
|||
return self._process
|
||||
if self._process:
|
||||
await self._process.stop()
|
||||
self._process = NanobotApiProcess(bus=self.bus, socket_path=self._socket_path)
|
||||
if self._reconnect_task and not self._reconnect_task.done():
|
||||
self._reconnect_task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await self._reconnect_task
|
||||
self._reconnect_task = None
|
||||
self._process = self._new_process()
|
||||
await self._process.start()
|
||||
if not self._process.running or not self._process.matches_current_socket():
|
||||
raise RuntimeError("Not connected to nanobot.")
|
||||
|
|
@ -312,6 +398,12 @@ class SuperTonicGateway:
|
|||
|
||||
async def disconnect_nanobot(self) -> None:
|
||||
async with self._lock:
|
||||
self._shutdown = True
|
||||
if self._reconnect_task and not self._reconnect_task.done():
|
||||
self._reconnect_task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await self._reconnect_task
|
||||
self._reconnect_task = None
|
||||
if self._process:
|
||||
await self._process.stop()
|
||||
self._process = None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue