Module agents.inference.turn
VideoSDK Inference Gateway Turn Detection Plugin.
HTTP-based End-of-Utterance (EOU) detector that delegates inference to the
VideoSDK Inference Gateway's /v1/turn endpoint.
Three server-side backends are available via factory methods:
Turn.namo()— Namo Turn Detector v1 (multilingual, 23 languages).Turn.turnsense()— TurnSense / SmolLM2-135M (English).Turn.videosdk()— VideoSDK BERT-based detector (English).
All three return a real float probability. No model is loaded locally — the server handles downloads, caching, and inference.
Example
from videosdk.inference import Turn
turn = Turn.namo(language="en") # Namo, English-specific model turn = Turn.turnsense() # TurnSense turn = Turn.videosdk() # VideoSDK BERT
pipeline = CascadingPipeline(stt=stt, llm=llm, tts=tts, turn_detector=turn)
Classes
class Turn (*,
provider: str = 'videosdk',
model_id: str = 'namo-turn-detector-v1',
language: Optional[str] = None,
threshold: float = 0.7,
base_url: Optional[str] = None,
timeout: float = 10.0,
max_connection_attempts: int = 5,
fallback_probability: float = 0.7)-
Expand source code
class Turn(EOU): """ VideoSDK Inference Gateway Turn Detection Plugin. Delegates EOU scoring to the inference gateway's ``/v1/turn`` endpoint. No model is loaded locally — the server caches every Namo-Turn-Detector-v1 language variant, so first-request latency per language is bounded by tokenize + inference time on the server (typically <50ms). Args: provider: Inference provider identifier (default: ``"videosdk"``). model_id: Turn detection model identifier (default: ``"namo-turn-detector-v1"``). language: Optional language code (e.g. ``"en"``, ``"hi"``, ``"es"``). Omit for the multilingual model. threshold: EOU probability threshold (default: ``0.7``). base_url: Override for the inference gateway URL. timeout: Per-request timeout in seconds. max_connection_attempts: After this many consecutive failed requests (non-200, timeout, invalid payload, etc.) the client stops calling the gateway and returns ``fallback_probability`` for the rest of the session. The counter resets on the next successful response. Default: 5. fallback_probability: Probability returned once the breaker has tripped. Default ``0.7`` matches the default threshold, so a disabled turn detector lets the pipeline assume end-of-utterance rather than stalling. """ def __init__( self, *, provider: str = "videosdk", model_id: str = "namo-turn-detector-v1", language: Optional[str] = None, threshold: float = 0.7, base_url: Optional[str] = None, timeout: float = DEFAULT_TIMEOUT_SECONDS, max_connection_attempts: int = 5, fallback_probability: float = 0.7, ) -> None: super().__init__(threshold=threshold) self._videosdk_token = os.getenv("VIDEOSDK_AUTH_TOKEN") if not self._videosdk_token: raise ValueError( "VIDEOSDK_AUTH_TOKEN environment variable must be set for authentication" ) self.provider = provider self.model_id = model_id self.language = language self.base_url = (base_url or DEFAULT_TURN_HTTP_URL).rstrip("/") self.timeout = timeout self.max_connection_attempts: int = max(1, int(max_connection_attempts)) self.fallback_probability: float = float(fallback_probability) self._session: Optional[requests.Session] = None self._logged_first_success: bool = False # Circuit breaker: consecutive failed HTTP attempts. When it reaches # max_connection_attempts, every subsequent call returns # fallback_probability without hitting the network. self._consecutive_failures: int = 0 self._turn_disabled: bool = False logger.info( f"[InferenceTurn] Configured (base_url={self.base_url}, " f"provider={self.provider}, model={self.model_id}, " f"language={self.language or 'multilingual'}, threshold={threshold}, " f"max_connection_attempts={self.max_connection_attempts}, " f"fallback_probability={self.fallback_probability})" ) # ==================== Factory Methods ==================== @staticmethod def namo( *, language: Optional[str] = None, threshold: float = 0.7, base_url: Optional[str] = None, timeout: float = DEFAULT_TIMEOUT_SECONDS, max_connection_attempts: int = 5, fallback_probability: float = 0.7, ) -> "Turn": """ Create a Turn detector backed by the server-hosted Namo Turn Detector v1. Args: language: Optional language code (e.g. ``"en"``, ``"hi"``, ``"es"``). Omit for the multilingual model. threshold: EOU probability threshold (default: ``0.7``). base_url: Override for the inference gateway URL. timeout: Per-request timeout in seconds. max_connection_attempts: Failures before the breaker trips. Default: 5. fallback_probability: Probability emitted once disabled. Default: 0.7. """ return Turn( provider="videosdk", model_id="namo-turn-detector-v1", language=language, threshold=threshold, base_url=base_url, timeout=timeout, max_connection_attempts=max_connection_attempts, fallback_probability=fallback_probability, ) @staticmethod def turnsense( *, threshold: float = 0.7, base_url: Optional[str] = None, timeout: float = DEFAULT_TIMEOUT_SECONDS, max_connection_attempts: int = 5, fallback_probability: float = 0.7, ) -> "Turn": """ Create a Turn detector backed by the server-hosted TurnSense model (latishab/turnsense, SmolLM2-135M, English). Args: threshold: EOU probability threshold (default: ``0.7``). base_url: Override for the inference gateway URL. timeout: Per-request timeout in seconds. max_connection_attempts: Failures before the breaker trips. Default: 5. fallback_probability: Probability emitted once disabled. Default: 0.7. """ return Turn( provider="turnsense", model_id="latishab/turnsense", language=None, threshold=threshold, base_url=base_url, timeout=timeout, max_connection_attempts=max_connection_attempts, fallback_probability=fallback_probability, ) @staticmethod def videosdk( *, threshold: float = 0.7, base_url: Optional[str] = None, timeout: float = DEFAULT_TIMEOUT_SECONDS, max_connection_attempts: int = 5, fallback_probability: float = 0.7, ) -> "Turn": """ Create a Turn detector backed by the server-hosted VideoSDK BERT model (cdn.videosdk.live, English, binary classifier with softmax probability). Args: threshold: EOU probability threshold (default: ``0.7``). base_url: Override for the inference gateway URL. timeout: Per-request timeout in seconds. max_connection_attempts: Failures before the breaker trips. Default: 5. fallback_probability: Probability emitted once disabled. Default: 0.7. """ return Turn( provider="videosdk", model_id="videosdk-turn-detector-v1", language=None, threshold=threshold, base_url=base_url, timeout=timeout, max_connection_attempts=max_connection_attempts, fallback_probability=fallback_probability, ) # ==================== Circuit Breaker ==================== def _record_attempt_failure(self, reason: str) -> float: """ Increment the consecutive-failure counter and trip the breaker once it reaches ``max_connection_attempts``. Returns the probability value the caller should emit on this turn — ``0.0`` while pre-trip, or ``fallback_probability`` once disabled. """ if self._turn_disabled: return self.fallback_probability self._consecutive_failures += 1 logger.warning( f"[InferenceTurn] Attempt failure " f"{self._consecutive_failures}/{self.max_connection_attempts}: {reason}" ) if self._consecutive_failures >= self.max_connection_attempts: self._turn_disabled = True logger.error( f"[InferenceTurn] Disabled for session after " f"{self._consecutive_failures} failed attempts — emitting " f"probability={self.fallback_probability} for the rest of the " f"turns (provider={self.provider}, model={self.model_id})" ) return self.fallback_probability return 0.0 def _record_attempt_success(self) -> None: """Reset the consecutive-failure counter on a successful response.""" if self._consecutive_failures > 0: logger.info( f"[InferenceTurn] Recovered after {self._consecutive_failures} " f"failed attempt(s) — resetting counter" ) self._consecutive_failures = 0 # ==================== Core EOU Interface ==================== def get_eou_probability(self, chat_context: ChatContext) -> float: """ Return the EOU probability for the last user message in ``chat_context``. This runs a synchronous HTTP POST against the inference gateway because the base :class:`EOU` interface is synchronous and the caller invokes it from inside an ``asyncio`` task. Requests are expected to be short (<100 ms) and the timeout is capped by ``self.timeout``. After ``max_connection_attempts`` consecutive failures the client stops calling the gateway and returns ``fallback_probability`` for the rest of the session. """ if self._turn_disabled: return self.fallback_probability payload = self._build_payload(chat_context) if payload is None: return 0.0 try: session = self._get_session() url = f"{self.base_url}/v1/turn" headers = { "Content-Type": "application/json", "Authorization": f"Bearer {self._videosdk_token}", } response = session.post( url, json=payload, headers=headers, timeout=self.timeout ) if response.status_code != 200: logger.error( f"[InferenceTurn] HTTP {response.status_code}: {response.text}" ) self.emit("error", f"HTTP {response.status_code}: {response.text}") return self._record_attempt_failure( f"HTTP {response.status_code}" ) data = response.json() probability = data.get("probability", 0.0) if not isinstance(probability, (int, float)): logger.error(f"[InferenceTurn] Invalid probability: {probability!r}") return self._record_attempt_failure("invalid probability payload") probability_f = float(probability) # A zero probability typically means the server omitted the field # (default 0.0 from .get) or returned a placeholder payload with # no real model signal. Treat it as a failed attempt for breaker # purposes, but keep returning 0.0 downstream while pre-trip so # EOU logic still sees "no end-of-utterance" as before. if probability_f == 0.0: return self._record_attempt_failure("probability=0.0") self._record_attempt_success() if not self._logged_first_success: self._logged_first_success = True logger.info( f"[InferenceTurn] Connected successfully to {self.base_url}/v1/turn " f"(first probability={probability_f:.4f})" ) return probability_f except requests.Timeout: logger.error(f"[InferenceTurn] Request timed out after {self.timeout}s") self.emit("error", "turn detection request timed out") return self._record_attempt_failure("timeout") except requests.RequestException as e: logger.error(f"[InferenceTurn] Request failed: {e}") self.emit("error", f"turn detection request failed: {e}") return self._record_attempt_failure(f"request exception: {e}") except Exception as e: logger.error(f"[InferenceTurn] Unexpected error: {e}") self.emit("error", f"turn detection error: {e}") return self._record_attempt_failure(f"unexpected error: {e}") # ==================== Helpers ==================== def _get_session(self) -> requests.Session: if self._session is None: self._session = requests.Session() return self._session def _build_payload(self, chat_context: ChatContext) -> Optional[Dict[str, Any]]: """Build the /v1/turn request body from a ChatContext.""" items: List[Dict[str, Any]] = [] for item in chat_context.items: if not isinstance(item, ChatMessage): continue if item.role != ChatRole.USER: continue text = self._content_to_text(item.content) if text: items.append({"role": "user", "content": text}) if not items: return None # Only the last user message is inspected by the server, but send the # full user-turn list to match the documented request shape. payload: Dict[str, Any] = { "chatContext": {"items": items}, "provider": self.provider, "modelId": self.model_id, } if self.language: payload["language"] = self.language return payload @staticmethod def _content_to_text(content: Any) -> str: if isinstance(content, str): return content.strip() if isinstance(content, list): parts: List[str] = [] for c in content: if hasattr(c, "text"): parts.append(str(getattr(c, "text") or "")) elif isinstance(c, str): parts.append(c) return " ".join(p for p in parts if p).strip() return str(content or "").strip() # ==================== Cleanup ==================== async def aclose(self) -> None: logger.info(f"[InferenceTurn] Closing Turn detector (language={self.language})") if self._session is not None: try: self._session.close() except Exception as e: logger.error(f"[InferenceTurn] Error closing HTTP session: {e}") self._session = None await super().aclose() # ==================== Properties ==================== @property def label(self) -> str: lang = self.language or "multilingual" return f"videosdk.inference.Turn.{self.provider}.{self.model_id}.{lang}"VideoSDK Inference Gateway Turn Detection Plugin.
Delegates EOU scoring to the inference gateway's
/v1/turnendpoint. No model is loaded locally — the server caches every Namo-Turn-Detector-v1 language variant, so first-request latency per language is bounded by tokenize + inference time on the server (typically <50ms).Args
provider- Inference provider identifier (default:
"videosdk"). model_id- Turn detection model identifier
(default:
"namo-turn-detector-v1"). language- Optional language code (e.g.
"en","hi","es"). Omit for the multilingual model. threshold- EOU probability threshold (default:
0.7). base_url- Override for the inference gateway URL.
timeout- Per-request timeout in seconds.
max_connection_attempts- After this many consecutive failed requests
(non-200, timeout, invalid payload, etc.) the client stops calling
the gateway and returns
fallback_probabilityfor the rest of the session. The counter resets on the next successful response. Default: 5. fallback_probability- Probability returned once the breaker has
tripped. Default
0.7matches the default threshold, so a disabled turn detector lets the pipeline assume end-of-utterance rather than stalling.
Ancestors
- videosdk.agents.eou.EOU
- videosdk.agents.event_emitter.EventEmitter
- typing.Generic
Static methods
def namo(*,
language: Optional[str] = None,
threshold: float = 0.7,
base_url: Optional[str] = None,
timeout: float = 10.0,
max_connection_attempts: int = 5,
fallback_probability: float = 0.7) ‑> Turn-
Expand source code
@staticmethod def namo( *, language: Optional[str] = None, threshold: float = 0.7, base_url: Optional[str] = None, timeout: float = DEFAULT_TIMEOUT_SECONDS, max_connection_attempts: int = 5, fallback_probability: float = 0.7, ) -> "Turn": """ Create a Turn detector backed by the server-hosted Namo Turn Detector v1. Args: language: Optional language code (e.g. ``"en"``, ``"hi"``, ``"es"``). Omit for the multilingual model. threshold: EOU probability threshold (default: ``0.7``). base_url: Override for the inference gateway URL. timeout: Per-request timeout in seconds. max_connection_attempts: Failures before the breaker trips. Default: 5. fallback_probability: Probability emitted once disabled. Default: 0.7. """ return Turn( provider="videosdk", model_id="namo-turn-detector-v1", language=language, threshold=threshold, base_url=base_url, timeout=timeout, max_connection_attempts=max_connection_attempts, fallback_probability=fallback_probability, )Create a Turn detector backed by the server-hosted Namo Turn Detector v1.
Args
language- Optional language code (e.g.
"en","hi","es"). Omit for the multilingual model. threshold- EOU probability threshold (default:
0.7). base_url- Override for the inference gateway URL.
timeout- Per-request timeout in seconds.
max_connection_attempts- Failures before the breaker trips. Default: 5.
fallback_probability- Probability emitted once disabled. Default: 0.7.
def turnsense(*,
threshold: float = 0.7,
base_url: Optional[str] = None,
timeout: float = 10.0,
max_connection_attempts: int = 5,
fallback_probability: float = 0.7) ‑> Turn-
Expand source code
@staticmethod def turnsense( *, threshold: float = 0.7, base_url: Optional[str] = None, timeout: float = DEFAULT_TIMEOUT_SECONDS, max_connection_attempts: int = 5, fallback_probability: float = 0.7, ) -> "Turn": """ Create a Turn detector backed by the server-hosted TurnSense model (latishab/turnsense, SmolLM2-135M, English). Args: threshold: EOU probability threshold (default: ``0.7``). base_url: Override for the inference gateway URL. timeout: Per-request timeout in seconds. max_connection_attempts: Failures before the breaker trips. Default: 5. fallback_probability: Probability emitted once disabled. Default: 0.7. """ return Turn( provider="turnsense", model_id="latishab/turnsense", language=None, threshold=threshold, base_url=base_url, timeout=timeout, max_connection_attempts=max_connection_attempts, fallback_probability=fallback_probability, )Create a Turn detector backed by the server-hosted TurnSense model (latishab/turnsense, SmolLM2-135M, English).
Args
threshold- EOU probability threshold (default:
0.7). base_url- Override for the inference gateway URL.
timeout- Per-request timeout in seconds.
max_connection_attempts- Failures before the breaker trips. Default: 5.
fallback_probability- Probability emitted once disabled. Default: 0.7.
def videosdk(*,
threshold: float = 0.7,
base_url: Optional[str] = None,
timeout: float = 10.0,
max_connection_attempts: int = 5,
fallback_probability: float = 0.7) ‑> Turn-
Expand source code
@staticmethod def videosdk( *, threshold: float = 0.7, base_url: Optional[str] = None, timeout: float = DEFAULT_TIMEOUT_SECONDS, max_connection_attempts: int = 5, fallback_probability: float = 0.7, ) -> "Turn": """ Create a Turn detector backed by the server-hosted VideoSDK BERT model (cdn.videosdk.live, English, binary classifier with softmax probability). Args: threshold: EOU probability threshold (default: ``0.7``). base_url: Override for the inference gateway URL. timeout: Per-request timeout in seconds. max_connection_attempts: Failures before the breaker trips. Default: 5. fallback_probability: Probability emitted once disabled. Default: 0.7. """ return Turn( provider="videosdk", model_id="videosdk-turn-detector-v1", language=None, threshold=threshold, base_url=base_url, timeout=timeout, max_connection_attempts=max_connection_attempts, fallback_probability=fallback_probability, )Create a Turn detector backed by the server-hosted VideoSDK BERT model (cdn.videosdk.live, English, binary classifier with softmax probability).
Args
threshold- EOU probability threshold (default:
0.7). base_url- Override for the inference gateway URL.
timeout- Per-request timeout in seconds.
max_connection_attempts- Failures before the breaker trips. Default: 5.
fallback_probability- Probability emitted once disabled. Default: 0.7.
Instance variables
prop label : str-
Expand source code
@property def label(self) -> str: lang = self.language or "multilingual" return f"videosdk.inference.Turn.{self.provider}.{self.model_id}.{lang}"Get the EOU provider label
Methods
async def aclose(self) ‑> None-
Expand source code
async def aclose(self) -> None: logger.info(f"[InferenceTurn] Closing Turn detector (language={self.language})") if self._session is not None: try: self._session.close() except Exception as e: logger.error(f"[InferenceTurn] Error closing HTTP session: {e}") self._session = None await super().aclose()Cleanup resources - should be overridden by subclasses to cleanup models
def get_eou_probability(self, chat_context: ChatContext) ‑> float-
Expand source code
def get_eou_probability(self, chat_context: ChatContext) -> float: """ Return the EOU probability for the last user message in ``chat_context``. This runs a synchronous HTTP POST against the inference gateway because the base :class:`EOU` interface is synchronous and the caller invokes it from inside an ``asyncio`` task. Requests are expected to be short (<100 ms) and the timeout is capped by ``self.timeout``. After ``max_connection_attempts`` consecutive failures the client stops calling the gateway and returns ``fallback_probability`` for the rest of the session. """ if self._turn_disabled: return self.fallback_probability payload = self._build_payload(chat_context) if payload is None: return 0.0 try: session = self._get_session() url = f"{self.base_url}/v1/turn" headers = { "Content-Type": "application/json", "Authorization": f"Bearer {self._videosdk_token}", } response = session.post( url, json=payload, headers=headers, timeout=self.timeout ) if response.status_code != 200: logger.error( f"[InferenceTurn] HTTP {response.status_code}: {response.text}" ) self.emit("error", f"HTTP {response.status_code}: {response.text}") return self._record_attempt_failure( f"HTTP {response.status_code}" ) data = response.json() probability = data.get("probability", 0.0) if not isinstance(probability, (int, float)): logger.error(f"[InferenceTurn] Invalid probability: {probability!r}") return self._record_attempt_failure("invalid probability payload") probability_f = float(probability) # A zero probability typically means the server omitted the field # (default 0.0 from .get) or returned a placeholder payload with # no real model signal. Treat it as a failed attempt for breaker # purposes, but keep returning 0.0 downstream while pre-trip so # EOU logic still sees "no end-of-utterance" as before. if probability_f == 0.0: return self._record_attempt_failure("probability=0.0") self._record_attempt_success() if not self._logged_first_success: self._logged_first_success = True logger.info( f"[InferenceTurn] Connected successfully to {self.base_url}/v1/turn " f"(first probability={probability_f:.4f})" ) return probability_f except requests.Timeout: logger.error(f"[InferenceTurn] Request timed out after {self.timeout}s") self.emit("error", "turn detection request timed out") return self._record_attempt_failure("timeout") except requests.RequestException as e: logger.error(f"[InferenceTurn] Request failed: {e}") self.emit("error", f"turn detection request failed: {e}") return self._record_attempt_failure(f"request exception: {e}") except Exception as e: logger.error(f"[InferenceTurn] Unexpected error: {e}") self.emit("error", f"turn detection error: {e}") return self._record_attempt_failure(f"unexpected error: {e}")Return the EOU probability for the last user message in
chat_context.This runs a synchronous HTTP POST against the inference gateway because the base :class:
EOUinterface is synchronous and the caller invokes it from inside anasynciotask. Requests are expected to be short (<100 ms) and the timeout is capped byself.timeout.After
max_connection_attemptsconsecutive failures the client stops calling the gateway and returnsfallback_probabilityfor the rest of the session.