Spaces:
Runtime error
Runtime error
File size: 5,409 Bytes
02422e3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | """
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 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@dataclass
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
|