mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
feat(realtime guardrails): add end_session_after_n_fails + Endpoint Settings UI step
- Replace check_every_n_turns with end_session_after_n_fails: session closes after N violations instead of checking on a turn schedule - Add _violation_count to RealTimeStreaming; increment on each guardrail block - _handle_violation_action now takes explicit end_session bool computed at call site (on_violation=end_session OR violation count hits threshold) - Add end_session_after_n_fails field to BaseLitellmParams, CustomGuardrail, and litellm_content_filter initializer - Remove stale debug print statements - Add 2 new tests: end_session_after_n_fails threshold and on_violation=end_session UI: add "Endpoint Settings (Optional)" as step 5 in the guardrail wizard - Call type dropdown uses /v1/realtime (endpoint path, not marketing name) - Realtime settings live inside a collapsed accordion, closed by default - "End session after X violations" replaces the N-turns field
This commit is contained in:
parent
a7f1b840b1
commit
ad57bf26bf
7 changed files with 366 additions and 50 deletions
|
|
@ -92,6 +92,9 @@ class CustomGuardrail(CustomLogger):
|
|||
mask_request_content: bool = False,
|
||||
mask_response_content: bool = False,
|
||||
violation_message_template: Optional[str] = None,
|
||||
end_session_after_n_fails: Optional[int] = None,
|
||||
on_violation: Optional[str] = None,
|
||||
realtime_violation_message: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
|
|
@ -104,6 +107,9 @@ class CustomGuardrail(CustomLogger):
|
|||
default_on: If True, the guardrail will be run by default on all requests
|
||||
mask_request_content: If True, the guardrail will mask the request content
|
||||
mask_response_content: If True, the guardrail will mask the response content
|
||||
end_session_after_n_fails: For Realtime API sessions, end the session after this many violations
|
||||
on_violation: For Realtime API sessions, 'warn' or 'end_session'
|
||||
realtime_violation_message: Message the bot speaks aloud when a Realtime guardrail fires
|
||||
"""
|
||||
self.guardrail_name = guardrail_name
|
||||
self.supported_event_hooks = supported_event_hooks
|
||||
|
|
@ -114,6 +120,9 @@ class CustomGuardrail(CustomLogger):
|
|||
self.mask_request_content: bool = mask_request_content
|
||||
self.mask_response_content: bool = mask_response_content
|
||||
self.violation_message_template: Optional[str] = violation_message_template
|
||||
self.end_session_after_n_fails: Optional[int] = end_session_after_n_fails
|
||||
self.on_violation: Optional[str] = on_violation
|
||||
self.realtime_violation_message: Optional[str] = realtime_violation_message
|
||||
|
||||
if supported_event_hooks:
|
||||
## validate event_hook is in supported_event_hooks
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ class RealTimeStreaming:
|
|||
self.user_api_key_dict = user_api_key_dict
|
||||
# Buffer for response.text.delta events pending output-guardrail check
|
||||
self._pending_output_text_events: List[str] = []
|
||||
# Violation counter for end_session_after_n_fails support
|
||||
self._violation_count: int = 0
|
||||
|
||||
def _should_store_message(
|
||||
self,
|
||||
|
|
@ -227,6 +229,37 @@ class RealTimeStreaming:
|
|||
for cb in litellm.callbacks
|
||||
)
|
||||
|
||||
async def _handle_violation_action(
|
||||
self, callback: Any, safe_msg: str, end_session: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Speak the violation message to the user, then optionally close the session.
|
||||
|
||||
end_session=True is set either when on_violation=='end_session' or when the
|
||||
session-level end_session_after_n_fails threshold has been reached.
|
||||
"""
|
||||
spoken_msg = getattr(callback, "realtime_violation_message", None) or safe_msg
|
||||
await self.backend_ws.send(json.dumps({"type": "response.cancel"}))
|
||||
await self.backend_ws.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "response.create",
|
||||
"response": {
|
||||
"modalities": ["text", "audio"],
|
||||
"instructions": (
|
||||
f"Say exactly and only: \"{spoken_msg}\". "
|
||||
"Do not add anything else."
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
if end_session:
|
||||
verbose_logger.warning(
|
||||
"[realtime guardrail] ending session after violation"
|
||||
)
|
||||
await self.backend_ws.close()
|
||||
|
||||
async def run_realtime_guardrails(
|
||||
self,
|
||||
transcript: str,
|
||||
|
|
@ -235,6 +268,10 @@ class RealTimeStreaming:
|
|||
"""
|
||||
Run registered guardrails on a completed speech transcription.
|
||||
|
||||
On each violation, increments the session violation counter.
|
||||
If end_session_after_n_fails is configured and the counter reaches that
|
||||
threshold, the session is terminated after speaking the violation message.
|
||||
|
||||
Returns True if blocked (synthetic warning already sent to client).
|
||||
Returns False if clean (caller should send response.create to the backend).
|
||||
"""
|
||||
|
|
@ -252,6 +289,7 @@ class RealTimeStreaming:
|
|||
is not True
|
||||
):
|
||||
continue
|
||||
|
||||
try:
|
||||
await callback.apply_guardrail(
|
||||
inputs={"texts": [transcript], "images": []},
|
||||
|
|
@ -276,26 +314,22 @@ class RealTimeStreaming:
|
|||
safe_msg = str(detail)
|
||||
else:
|
||||
safe_msg = str(e) or "I'm sorry, that request was blocked by the content filter."
|
||||
# Cancel any in-flight response before speaking the warning.
|
||||
# This handles the race where create_response fired before we could intercept.
|
||||
await self.backend_ws.send(json.dumps({"type": "response.cancel"}))
|
||||
# Ask OpenAI to speak the warning — TTS audio plays naturally in the client
|
||||
await self.backend_ws.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "response.create",
|
||||
"response": {
|
||||
"modalities": ["text", "audio"],
|
||||
"instructions": (
|
||||
f"Say exactly and only: \"{safe_msg}\". "
|
||||
"Do not add anything else."
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
self._violation_count += 1
|
||||
end_session_after: Optional[int] = getattr(
|
||||
callback, "end_session_after_n_fails", None
|
||||
)
|
||||
should_end = (
|
||||
getattr(callback, "on_violation", None) == "end_session"
|
||||
or (
|
||||
end_session_after is not None
|
||||
and self._violation_count >= end_session_after
|
||||
)
|
||||
)
|
||||
await self._handle_violation_action(callback, safe_msg, end_session=should_end)
|
||||
verbose_logger.warning(
|
||||
"[realtime guardrail] BLOCKED transcript: %r",
|
||||
"[realtime guardrail] BLOCKED transcript (violation %d): %r",
|
||||
self._violation_count,
|
||||
transcript[:80],
|
||||
)
|
||||
return True
|
||||
|
|
@ -494,7 +528,6 @@ class RealTimeStreaming:
|
|||
"""
|
||||
try:
|
||||
event_obj = json.loads(raw_response)
|
||||
|
||||
if event_obj.get("type") == "session.created":
|
||||
# If any realtime guardrails are registered, proactively
|
||||
# set create_response=false so the LLM never auto-responds
|
||||
|
|
@ -539,11 +572,22 @@ class RealTimeStreaming:
|
|||
return True
|
||||
|
||||
if event_obj.get("type") == "response.text.delta":
|
||||
if self._has_realtime_output_guardrails():
|
||||
has_guardrails = self._has_realtime_output_guardrails()
|
||||
verbose_logger.warning(
|
||||
"[realtime output guardrail] response.text.delta — _has_realtime_output_guardrails=%s callbacks=%s",
|
||||
has_guardrails,
|
||||
[type(c).__name__ for c in litellm.callbacks],
|
||||
)
|
||||
if has_guardrails:
|
||||
self._pending_output_text_events.append(raw_response)
|
||||
return True
|
||||
|
||||
if event_obj.get("type") == "response.text.done":
|
||||
verbose_logger.warning(
|
||||
"[realtime output guardrail] response.text.done — text=%r _has_guardrails=%s",
|
||||
event_obj.get("text", "")[:60],
|
||||
self._has_realtime_output_guardrails(),
|
||||
)
|
||||
await self._send_output_text_done(event_obj, raw_response)
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import \
|
||||
ContentFilterGuardrail
|
||||
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import (
|
||||
ContentFilterGuardrail,
|
||||
)
|
||||
from litellm.types.guardrails import SupportedGuardrailIntegrations
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -46,6 +47,11 @@ def initialize_guardrail(
|
|||
competitor_intent_config=getattr(
|
||||
litellm_params, "competitor_intent_config", None
|
||||
),
|
||||
end_session_after_n_fails=getattr(litellm_params, "end_session_after_n_fails", None),
|
||||
on_violation=getattr(litellm_params, "on_violation", None),
|
||||
realtime_violation_message=getattr(
|
||||
litellm_params, "realtime_violation_message", None
|
||||
),
|
||||
)
|
||||
|
||||
litellm.logging_callback_manager.add_litellm_callback(content_filter_guardrail)
|
||||
|
|
|
|||
|
|
@ -643,6 +643,21 @@ class BaseLitellmParams(
|
|||
description="Custom message when a guardrail blocks an action. Supports placeholders like {tool_name}, {rule_id}, and {default_message}.",
|
||||
)
|
||||
|
||||
################## Realtime API params ################
|
||||
########################################################
|
||||
end_session_after_n_fails: Optional[int] = Field(
|
||||
default=None,
|
||||
description="For Realtime API sessions: automatically close the session after this many guardrail violations in a single conversation.",
|
||||
)
|
||||
on_violation: Optional[Literal["warn", "end_session"]] = Field(
|
||||
default=None,
|
||||
description="For Realtime API sessions: 'warn' speaks the violation message and continues the session; 'end_session' speaks the message and closes the connection.",
|
||||
)
|
||||
realtime_violation_message: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The message the bot speaks aloud when a Realtime API guardrail fires. Falls back to violation_message_template if not set.",
|
||||
)
|
||||
|
||||
# Model Armor params
|
||||
template_id: Optional[str] = Field(
|
||||
default=None, description="The ID of your Model Armor template"
|
||||
|
|
|
|||
|
|
@ -569,3 +569,115 @@ async def test_realtime_session_created_injects_create_response_false():
|
|||
)
|
||||
|
||||
litellm.callbacks = [] # cleanup
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_end_session_after_n_fails_closes_connection():
|
||||
"""
|
||||
Test that end_session_after_n_fails=2 closes the backend websocket after
|
||||
the second guardrail violation in a session.
|
||||
"""
|
||||
import litellm
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
from litellm.types.guardrails import GuardrailEventHooks
|
||||
|
||||
class BadWordGuardrail(CustomGuardrail):
|
||||
async def apply_guardrail(self, inputs, request_data, input_type, logging_obj=None):
|
||||
for text in inputs.get("texts", []):
|
||||
if "blocked" in text.lower():
|
||||
raise ValueError("Content blocked by guardrail.")
|
||||
return inputs
|
||||
|
||||
guardrail = BadWordGuardrail(
|
||||
guardrail_name="bad_word_guard",
|
||||
event_hook=GuardrailEventHooks.pre_call,
|
||||
default_on=True,
|
||||
end_session_after_n_fails=2,
|
||||
)
|
||||
litellm.callbacks = [guardrail]
|
||||
|
||||
client_ws = MagicMock()
|
||||
client_ws.send_text = AsyncMock()
|
||||
|
||||
def make_transcript_event(text):
|
||||
return json.dumps({
|
||||
"type": "conversation.item.input_audio_transcription.completed",
|
||||
"transcript": text,
|
||||
"item_id": "item_x",
|
||||
}).encode()
|
||||
|
||||
backend_ws = MagicMock()
|
||||
backend_ws.recv = AsyncMock(
|
||||
side_effect=[
|
||||
make_transcript_event("this is blocked"), # violation 1 — warn
|
||||
make_transcript_event("also blocked again"), # violation 2 — end session
|
||||
ConnectionClosed(None, None),
|
||||
]
|
||||
)
|
||||
backend_ws.send = AsyncMock()
|
||||
backend_ws.close = AsyncMock()
|
||||
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.async_success_handler = AsyncMock()
|
||||
logging_obj.success_handler = MagicMock()
|
||||
streaming = RealTimeStreaming(client_ws, backend_ws, logging_obj)
|
||||
await streaming.backend_to_client_send_messages()
|
||||
|
||||
assert backend_ws.close.called, "Expected backend_ws.close() to be called after 2 violations"
|
||||
assert streaming._violation_count == 2
|
||||
|
||||
litellm.callbacks = [] # cleanup
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_violation_end_session_closes_on_first_fail():
|
||||
"""
|
||||
Test that on_violation='end_session' closes the session immediately on the
|
||||
first violation, regardless of end_session_after_n_fails.
|
||||
"""
|
||||
import litellm
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
from litellm.types.guardrails import GuardrailEventHooks
|
||||
|
||||
class TopicGuardrail(CustomGuardrail):
|
||||
async def apply_guardrail(self, inputs, request_data, input_type, logging_obj=None):
|
||||
for text in inputs.get("texts", []):
|
||||
if "stock" in text.lower():
|
||||
raise ValueError("Topic not allowed: financial advice.")
|
||||
return inputs
|
||||
|
||||
guardrail = TopicGuardrail(
|
||||
guardrail_name="topic_guard",
|
||||
event_hook=GuardrailEventHooks.pre_call,
|
||||
default_on=True,
|
||||
on_violation="end_session",
|
||||
)
|
||||
litellm.callbacks = [guardrail]
|
||||
|
||||
client_ws = MagicMock()
|
||||
client_ws.send_text = AsyncMock()
|
||||
|
||||
backend_ws = MagicMock()
|
||||
backend_ws.recv = AsyncMock(
|
||||
side_effect=[
|
||||
json.dumps({
|
||||
"type": "conversation.item.input_audio_transcription.completed",
|
||||
"transcript": "What stock should I buy today?",
|
||||
"item_id": "item_y",
|
||||
}).encode(),
|
||||
ConnectionClosed(None, None),
|
||||
]
|
||||
)
|
||||
backend_ws.send = AsyncMock()
|
||||
backend_ws.close = AsyncMock()
|
||||
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.async_success_handler = AsyncMock()
|
||||
logging_obj.success_handler = MagicMock()
|
||||
streaming = RealTimeStreaming(client_ws, backend_ws, logging_obj)
|
||||
await streaming.backend_to_client_send_messages()
|
||||
|
||||
assert backend_ws.close.called, "Expected session to close immediately with on_violation=end_session"
|
||||
assert streaming._violation_count == 1
|
||||
|
||||
litellm.callbacks = [] # cleanup
|
||||
|
|
|
|||
|
|
@ -2,19 +2,7 @@
|
|||
|
||||
import React, { useCallback, useDeferredValue, useEffect, useState } from "react";
|
||||
import { Select, Switch, Tooltip } from "antd";
|
||||
<<<<<<< cursor/development-environment-setup-13a7
|
||||
// @ts-ignore - duplicate import removed
|
||||
import {
|
||||
Table,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from "@tremor/react";
|
||||
=======
|
||||
import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react";
|
||||
>>>>>>> main
|
||||
import { TimeCell } from "./view_logs/time_cell";
|
||||
import { TableHeaderSortDropdown } from "./common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
|
||||
import type { SortState } from "./common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
|
||||
|
|
@ -60,21 +48,6 @@ const PolicySelect: React.FC<{
|
|||
minWidth: 110,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
<<<<<<< cursor/development-environment-setup-13a7
|
||||
{...{styles: {
|
||||
selector: {
|
||||
backgroundColor: style.bg,
|
||||
borderColor: style.border,
|
||||
color: style.color,
|
||||
borderRadius: 999,
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 4,
|
||||
},
|
||||
}} as any}
|
||||
=======
|
||||
>>>>>>> main
|
||||
popupMatchSelectWidth={false}
|
||||
options={POLICY_OPTIONS.map((o) => ({
|
||||
value: o.value,
|
||||
|
|
|
|||
|
|
@ -118,6 +118,13 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
const [pendingCategorySelection, setPendingCategorySelection] = useState<string>("");
|
||||
const [competitorIntentEnabled, setCompetitorIntentEnabled] = useState(false);
|
||||
const [competitorIntentConfig, setCompetitorIntentConfig] = useState<any>(null);
|
||||
|
||||
// Endpoint Settings state
|
||||
const [selectedEndpointType, setSelectedEndpointType] = useState<string>("");
|
||||
const [endSessionAfterNFails, setEndSessionAfterNFails] = useState<number | undefined>(undefined);
|
||||
const [onViolation, setOnViolation] = useState<"warn" | "end_session">("warn");
|
||||
const [realtimeViolationMessage, setRealtimeViolationMessage] = useState<string>("");
|
||||
const [endpointSettingsOpen, setEndpointSettingsOpen] = useState<boolean>(false);
|
||||
const [toolPermissionConfig, setToolPermissionConfig] = useState<ToolPermissionConfig>({
|
||||
rules: [],
|
||||
default_action: "deny",
|
||||
|
|
@ -353,6 +360,11 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
on_disallowed_action: "block",
|
||||
violation_message_template: "",
|
||||
});
|
||||
setSelectedEndpointType("");
|
||||
setEndSessionAfterNFails(undefined);
|
||||
setOnViolation("warn");
|
||||
setRealtimeViolationMessage("");
|
||||
setEndpointSettingsOpen(false);
|
||||
setCurrentStep(0);
|
||||
};
|
||||
|
||||
|
|
@ -482,6 +494,17 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
}
|
||||
}
|
||||
|
||||
// Endpoint Settings (realtime) — apply for content filter guardrails
|
||||
if (shouldRenderContentFilterConfigSettings(values.provider)) {
|
||||
if (endSessionAfterNFails !== undefined && endSessionAfterNFails > 0) {
|
||||
guardrailData.litellm_params.end_session_after_n_fails = endSessionAfterNFails;
|
||||
}
|
||||
guardrailData.litellm_params.on_violation = onViolation;
|
||||
if (realtimeViolationMessage.trim()) {
|
||||
guardrailData.litellm_params.realtime_violation_message = realtimeViolationMessage.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (guardrailProvider === "tool_permission") {
|
||||
if (toolPermissionConfig.rules.length === 0) {
|
||||
NotificationsManager.fromBackend("Add at least one tool permission rule");
|
||||
|
|
@ -811,6 +834,137 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
return <GuardrailOptionalParams optionalParams={providerFields.optional_params} parentFieldKey="optional_params" />;
|
||||
};
|
||||
|
||||
const renderEndpointSettings = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Section header */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">
|
||||
Configure settings for a specific call type. Most guardrails don't need this — skip it
|
||||
unless you're using a specific endpoint like <code>/v1/realtime</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Endpoint selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Call type
|
||||
</label>
|
||||
<Select
|
||||
placeholder="Select a call type"
|
||||
value={selectedEndpointType || undefined}
|
||||
onChange={(v) => {
|
||||
setSelectedEndpointType(v);
|
||||
setEndpointSettingsOpen(false);
|
||||
}}
|
||||
style={{ width: 260 }}
|
||||
allowClear
|
||||
options={[
|
||||
{
|
||||
value: "realtime",
|
||||
label: "/v1/realtime",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
More call types coming soon.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Realtime-specific settings — accordion, closed by default */}
|
||||
{selectedEndpointType === "realtime" && (
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEndpointSettingsOpen((o) => !o)}
|
||||
className="w-full flex items-center justify-between px-4 py-3 bg-gray-50 hover:bg-gray-100 text-sm font-medium text-gray-700"
|
||||
>
|
||||
<span>/v1/realtime settings</span>
|
||||
<svg
|
||||
className={`w-4 h-4 text-gray-500 transition-transform ${endpointSettingsOpen ? "rotate-180" : ""}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{endpointSettingsOpen && (
|
||||
<div className="space-y-5 px-4 py-4 border-t border-gray-200">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
End session after X violations
|
||||
</label>
|
||||
<p className="text-xs text-gray-400 mb-2">
|
||||
Automatically close the session after this many guardrail violations in a single
|
||||
conversation. Leave empty to never auto-close.
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="e.g. 3"
|
||||
value={endSessionAfterNFails ?? ""}
|
||||
onChange={(e) =>
|
||||
setEndSessionAfterNFails(e.target.value ? parseInt(e.target.value, 10) : undefined)
|
||||
}
|
||||
className="border border-gray-300 rounded px-3 py-1.5 text-sm w-32"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">On violation</label>
|
||||
<div className="space-y-2">
|
||||
{(["warn", "end_session"] as const).map((opt) => (
|
||||
<label key={opt} className="flex items-start gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="on_violation"
|
||||
value={opt}
|
||||
checked={onViolation === opt}
|
||||
onChange={() => setOnViolation(opt)}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-800">
|
||||
{opt === "warn" ? "Warn" : "End session"}
|
||||
</span>
|
||||
<p className="text-xs text-gray-400 m-0">
|
||||
{opt === "warn"
|
||||
? "Bot speaks the message, session continues"
|
||||
: "Bot speaks the message, connection closes"}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Message the user hears
|
||||
</label>
|
||||
<p className="text-xs text-gray-400 mb-2">
|
||||
What the bot says out loud when this guardrail fires. Falls back to the default
|
||||
violation message if empty.
|
||||
</p>
|
||||
<textarea
|
||||
rows={3}
|
||||
placeholder="e.g. I'm not able to continue this conversation. Please contact us at 1-800-774-2678."
|
||||
value={realtimeViolationMessage}
|
||||
onChange={(e) => setRealtimeViolationMessage(e.target.value)}
|
||||
className="border border-gray-300 rounded px-3 py-2 text-sm w-full resize-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 0:
|
||||
|
|
@ -833,13 +987,15 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
return renderContentFilterConfiguration("keywords");
|
||||
}
|
||||
return null;
|
||||
case 4:
|
||||
return renderEndpointSettings();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderStepButtons = () => {
|
||||
const totalSteps = shouldRenderContentFilterConfigSettings(selectedProvider) ? 4 : 2;
|
||||
const totalSteps = shouldRenderContentFilterConfigSettings(selectedProvider) ? 5 : 2;
|
||||
const isLastStep = currentStep === totalSteps - 1;
|
||||
const isCategoriesStep = shouldRenderContentFilterConfigSettings(selectedProvider) && currentStep === 1;
|
||||
const hasPendingCategory = pendingCategorySelection !== "";
|
||||
|
|
@ -884,9 +1040,10 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
if (shouldRenderContentFilterConfigSettings(selectedProvider)) {
|
||||
return [
|
||||
{ title: "Basic Info", optional: false },
|
||||
{ title: "Default Categories", optional: false },
|
||||
{ title: "Topics", optional: false },
|
||||
{ title: "Patterns", optional: false },
|
||||
{ title: "Keywords", optional: false },
|
||||
{ title: "Endpoint Settings (Optional)", optional: true },
|
||||
];
|
||||
}
|
||||
if (shouldRenderPIIConfigSettings(selectedProvider)) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue