Spaces:
Runtime error
Runtime error
| """ | |
| sidecar/stream_monitor.py β Async sentence-level post-inference monitor. | |
| Wraps the synchronous TextMonitor in an async ThreadPoolExecutor so that | |
| per-sentence analysis runs concurrently in the background while the LLM | |
| stream continues flowing to the client. | |
| When a sentence crosses the threat threshold, a BlockSignal is returned | |
| so the SSE layer can push a block event to the client immediately. | |
| """ | |
| import asyncio | |
| import logging | |
| from concurrent.futures import ThreadPoolExecutor | |
| from dataclasses import dataclass, field | |
| from typing import List, Optional | |
| log = logging.getLogger(__name__) | |
| # ββ Event types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class BlockSignal: | |
| """Emitted when the post-monitor flags a sentence as harmful.""" | |
| sentence_id: int | |
| reason: str | |
| threat_score: int | |
| flags: List[str] | |
| type: str = field(default="block_signal", init=False) | |
| # ββ Stream Monitor βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class StreamMonitor: | |
| """ | |
| Async wrapper around TextMonitor for sentence-level background screening. | |
| Usage | |
| ----- | |
| monitor = StreamMonitor(text_monitor_instance, block_threshold=40) | |
| # Submit sentences as they complete (non-blocking): | |
| task = await monitor.submit(sentence_id=1, sentence="...", prompt="...") | |
| # At stream end β collect any pending block signals: | |
| signals = await monitor.collect(timeout=1.0) | |
| Args: | |
| text_monitor: Existing TextMonitor instance. | |
| block_threshold: threat_score at or above which a BlockSignal is raised. | |
| max_workers: Thread pool size for concurrent sentence analysis. | |
| """ | |
| def __init__( | |
| self, | |
| text_monitor, | |
| block_threshold: int = 40, | |
| max_workers: int = 4, | |
| ): | |
| self._monitor = text_monitor | |
| self._threshold = block_threshold | |
| self._executor = ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix="shield-monitor") | |
| self._tasks: List[asyncio.Future] = [] | |
| # ------------------------------------------------------------------ | |
| # Public API | |
| # ------------------------------------------------------------------ | |
| async def submit(self, sentence_id: int, sentence: str, prompt: str) -> None: | |
| """ | |
| Non-blocking: submit a sentence for background analysis. | |
| The result is stored internally; call collect() to retrieve signals. | |
| """ | |
| loop = asyncio.get_event_loop() | |
| future = loop.run_in_executor( | |
| self._executor, | |
| self._analyze_sync, | |
| sentence_id, sentence, prompt, | |
| ) | |
| self._tasks.append(future) | |
| async def collect(self, timeout: float = 1.5) -> List[BlockSignal]: | |
| """ | |
| Wait for all pending monitor tasks (up to timeout) and return | |
| any BlockSignals found. | |
| Called at stream end to finalise the threat assessment. | |
| """ | |
| if not self._tasks: | |
| return [] | |
| signals: List[BlockSignal] = [] | |
| try: | |
| results = await asyncio.wait_for( | |
| asyncio.gather(*self._tasks, return_exceptions=True), | |
| timeout=timeout, | |
| ) | |
| for result in results: | |
| if isinstance(result, BlockSignal): | |
| signals.append(result) | |
| elif isinstance(result, Exception): | |
| log.warning("[StreamMonitor] Task error: %s", result) | |
| except asyncio.TimeoutError: | |
| log.warning("[StreamMonitor] collect() timed out after %.1fs β some sentences unscreened", timeout) | |
| self._tasks.clear() | |
| return signals | |
| def reset(self) -> None: | |
| """Clear pending tasks (e.g. between requests).""" | |
| self._tasks.clear() | |
| def shutdown(self) -> None: | |
| """Gracefully shut down the thread pool.""" | |
| self._executor.shutdown(wait=False) | |
| # ------------------------------------------------------------------ | |
| # Internal β runs in thread pool | |
| # ------------------------------------------------------------------ | |
| def _analyze_sync(self, sentence_id: int, sentence: str, prompt: str) -> Optional[BlockSignal]: | |
| """ | |
| Synchronous analysis β called inside ThreadPoolExecutor. | |
| Returns a BlockSignal if the sentence is flagged, else None. | |
| """ | |
| try: | |
| result = self._monitor.analyze(prompt=prompt, response=sentence) | |
| if result["threat_score"] >= self._threshold: | |
| log.warning( | |
| "[StreamMonitor] Sentence %d flagged: score=%d flags=%s", | |
| sentence_id, result["threat_score"], result["flags"], | |
| ) | |
| return BlockSignal( | |
| sentence_id = sentence_id, | |
| reason = result["reason"], | |
| threat_score = result["threat_score"], | |
| flags = result["flags"], | |
| ) | |
| except Exception as exc: | |
| log.error("[StreamMonitor] Analysis error on sentence %d: %s", sentence_id, exc) | |
| return None | |