mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
feat: ui tweaks for prod
This commit is contained in:
parent
027b92b27a
commit
434ff5289d
12 changed files with 685 additions and 218 deletions
|
|
@ -827,6 +827,42 @@ async def get_category_yaml(category_name: str):
|
|||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/guardrails/ui/major_airlines",
|
||||
tags=["Guardrails"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def get_major_airlines():
|
||||
"""
|
||||
Get the major airlines list from IATA (competitor intent, airline type).
|
||||
Returns airline id, match variants (pipe-separated), and tags.
|
||||
"""
|
||||
import os
|
||||
|
||||
airlines_path = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"guardrail_hooks",
|
||||
"litellm_content_filter",
|
||||
"competitor_intent",
|
||||
"major_airlines.json",
|
||||
)
|
||||
if not os.path.exists(airlines_path):
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="major_airlines.json not found",
|
||||
)
|
||||
try:
|
||||
with open(airlines_path, "r", encoding="utf-8") as f:
|
||||
import json
|
||||
|
||||
airlines = json.load(f)
|
||||
return {"airlines": airlines}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error reading major_airlines.json: {str(e)}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.post(
|
||||
"/guardrails/validate_blocked_words_file",
|
||||
tags=["Guardrails"],
|
||||
|
|
|
|||
|
|
@ -10,19 +10,8 @@ import json
|
|||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncGenerator,
|
||||
Dict,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Pattern,
|
||||
Tuple,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
from typing import (TYPE_CHECKING, Any, AsyncGenerator, Dict, List, Literal,
|
||||
Optional, Pattern, Tuple, Union, cast)
|
||||
|
||||
import yaml
|
||||
from fastapi import HTTPException
|
||||
|
|
@ -31,34 +20,22 @@ from litellm import Router
|
|||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.types.utils import (
|
||||
GenericGuardrailAPIInputs,
|
||||
GuardrailStatus,
|
||||
GuardrailTracingDetail,
|
||||
ModelResponseStream,
|
||||
)
|
||||
from litellm.types.utils import (GenericGuardrailAPIInputs, GuardrailStatus,
|
||||
GuardrailTracingDetail, ModelResponseStream)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
|
||||
from litellm.types.guardrails import (
|
||||
BlockedWord,
|
||||
ContentFilterAction,
|
||||
ContentFilterPattern,
|
||||
GuardrailEventHooks,
|
||||
Mode,
|
||||
)
|
||||
from litellm.types.guardrails import (BlockedWord, ContentFilterAction,
|
||||
ContentFilterPattern,
|
||||
GuardrailEventHooks, Mode)
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter import (
|
||||
BlockedWordDetection,
|
||||
CategoryKeywordDetection,
|
||||
CompetitorIntentDetection,
|
||||
CompetitorIntentResult,
|
||||
ContentFilterCategoryConfig,
|
||||
ContentFilterDetection,
|
||||
PatternDetection,
|
||||
)
|
||||
BlockedWordDetection, CategoryKeywordDetection, CompetitorIntentDetection,
|
||||
CompetitorIntentResult, ContentFilterCategoryConfig,
|
||||
ContentFilterDetection, PatternDetection)
|
||||
|
||||
from .competitor_intent import AirlineCompetitorIntentChecker
|
||||
from .competitor_intent import (AirlineCompetitorIntentChecker,
|
||||
BaseCompetitorIntentChecker)
|
||||
from .patterns import PATTERN_EXTRA_CONFIG, get_compiled_pattern
|
||||
|
||||
MAX_KEYWORD_VALUE_GAP_WORDS = 1
|
||||
|
|
@ -206,15 +183,24 @@ class ContentFilterGuardrail(CustomGuardrail):
|
|||
{}
|
||||
) # category_name -> {identifier_words, block_words, action, severity}
|
||||
|
||||
# Competitor intent checker (optional, generic; industry presets add domain_words etc.)
|
||||
self._competitor_intent_checker: Optional[AirlineCompetitorIntentChecker] = None
|
||||
# Competitor intent checker (optional; airline uses major_airlines.json, generic requires competitors)
|
||||
self._competitor_intent_checker: Optional[BaseCompetitorIntentChecker] = None
|
||||
if competitor_intent_config and isinstance(competitor_intent_config, dict):
|
||||
try:
|
||||
self._competitor_intent_checker = AirlineCompetitorIntentChecker(
|
||||
competitor_intent_config
|
||||
competitor_intent_type = competitor_intent_config.get(
|
||||
"competitor_intent_type", "airline"
|
||||
)
|
||||
if competitor_intent_type == "generic":
|
||||
self._competitor_intent_checker = BaseCompetitorIntentChecker(
|
||||
competitor_intent_config
|
||||
)
|
||||
else:
|
||||
self._competitor_intent_checker = AirlineCompetitorIntentChecker(
|
||||
competitor_intent_config
|
||||
)
|
||||
verbose_proxy_logger.debug(
|
||||
"ContentFilterGuardrail: competitor intent checker enabled"
|
||||
"ContentFilterGuardrail: competitor intent checker enabled (%s)",
|
||||
competitor_intent_type,
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.warning(
|
||||
|
|
@ -1822,8 +1808,7 @@ class ContentFilterGuardrail(CustomGuardrail):
|
|||
|
||||
@staticmethod
|
||||
def get_config_model():
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter import (
|
||||
LitellmContentFilterGuardrailConfigModel,
|
||||
)
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter import \
|
||||
LitellmContentFilterGuardrailConfigModel
|
||||
|
||||
return LitellmContentFilterGuardrailConfigModel
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
# Content Filter Examples
|
||||
|
||||
## Industry-specific competitor intent: Emirates (airline)
|
||||
## Industry-specific competitor intent (airline)
|
||||
|
||||
The **generic** competitor intent blocker only needs:
|
||||
Use **competitor_intent_type: airline** for simplified config; competitors are auto-loaded from IATA `major_airlines.json`, excluding your `brand_self`. Use **competitor_intent_type: generic** when you want to specify competitors manually.
|
||||
|
||||
- **brand_self**: Your brand names and aliases (e.g. `["emirates", "ek"]`)
|
||||
- **competitors**: List of competitor names (e.g. `["qatar airways", "etihad"]`)
|
||||
- **brand_self**: Your brand names and aliases (e.g. `["your-brand", "your-code"]`)
|
||||
- **competitors** (generic only): List of competitor names
|
||||
|
||||
To make it effective for a specific industry (e.g. airlines), add an **industry layer** on top:
|
||||
|
||||
1. **domain_words** – Terms that signal “this is about our vertical.”
|
||||
For Emirates (airline): `airline`, `carrier`, `flight`, `business class`, `lounge`, etc.
|
||||
For airline: `airline`, `carrier`, `flight`, `business class`, `lounge`, etc.
|
||||
This enables the **category_ranking** path (e.g. “Which Gulf airline is the best?”) and the scoring **gate** (so “best” alone doesn’t trigger without domain/geo).
|
||||
|
||||
2. **route_geo_cues** – Optional geography/hub terms.
|
||||
For Emirates: `doha`, `dubai`, `abu dhabi`, `gulf`, `middle east`.
|
||||
For airline: `country`, `hub-city`, `airport-code`, `region`.
|
||||
|
||||
3. **descriptor_lexicon** – Phrases that count as indirect competitor reference.
|
||||
For aviation: `doha airline`, `oryx airline`, `gulf carrier`, `five star airline`, `skytrax`.
|
||||
For aviation: `gulf carrier`, `five star airline`, etc.
|
||||
|
||||
4. **competitor_aliases** – Per-competitor aliases (IATA codes, nicknames).
|
||||
Example: `qatar airways` → `["qr", "doha airline"]`, `etihad` → `["ey"]`.
|
||||
Example: `competitor-name` → `["iata-code", "nickname"]`.
|
||||
|
||||
5. **policy** – What to do per intent band: `refuse`, `reframe`, `log_only`, or `allow`.
|
||||
Example: `competitor_comparison: refuse`, `category_ranking: reframe`.
|
||||
|
||||
See **emirates_competitor_intent_guardrail.yaml** for a full example you can copy into your proxy `guardrails` config or merge into an existing `litellm_content_filter` guardrail.
|
||||
See the config examples below for how to add this to your proxy `guardrails` config.
|
||||
|
||||
### Using the example in your proxy config
|
||||
|
||||
|
|
@ -33,27 +33,20 @@ In `config.yaml`:
|
|||
|
||||
```yaml
|
||||
guardrails:
|
||||
- guardrail_name: "emirates-competitor-intent"
|
||||
- guardrail_name: "airline-competitor-intent"
|
||||
litellm_params:
|
||||
guardrail: litellm_content_filter
|
||||
mode: pre_call
|
||||
competitor_intent_config:
|
||||
brand_self: [emirates, ek]
|
||||
competitors: [qatar airways, qatar, etihad, turkish airlines]
|
||||
domain_words: [airline, carrier, flight, business class, lounge]
|
||||
route_geo_cues: [doha, dubai, abu dhabi, gulf]
|
||||
descriptor_lexicon: [doha airline, gulf carrier, five star airline]
|
||||
competitor_aliases:
|
||||
qatar airways: [qr, doha airline]
|
||||
etihad: [ey]
|
||||
competitor_intent_type: airline
|
||||
brand_self: [your-brand, your-code]
|
||||
locations: [relevant-country, hub-city]
|
||||
policy:
|
||||
competitor_comparison: refuse
|
||||
possible_competitor_comparison: reframe
|
||||
category_ranking: reframe
|
||||
log_only: log_only
|
||||
```
|
||||
|
||||
Then attach this guardrail to your router/policy (e.g. `guardrails.add: [emirates-competitor-intent]`).
|
||||
Then attach this guardrail to your router/policy (e.g. `guardrails.add: [airline-competitor-intent]`).
|
||||
|
||||
### Other industries
|
||||
|
||||
|
|
|
|||
|
|
@ -1,134 +0,0 @@
|
|||
# Emirates (airline) industry-specific competitor intent guardrail
|
||||
#
|
||||
# This is an example of building on the generic competitor intent blocker with
|
||||
# industry-specific inputs: domain_words, route_geo_cues, descriptor_lexicon,
|
||||
# and competitor_aliases for aviation.
|
||||
#
|
||||
# Use this guardrail in your proxy config under guardrails.litellm_params, or
|
||||
# merge competitor_intent_config into an existing litellm_content_filter guardrail.
|
||||
#
|
||||
# Minimal required (generic): brand_self, competitors
|
||||
# Industry layer (airline): domain_words, route_geo_cues, descriptor_lexicon,
|
||||
# competitor_aliases (IATA/descriptive), optional indirect_competitor_patterns
|
||||
|
||||
guardrail_name: "emirates-competitor-intent"
|
||||
guardrail_id: "emirates-competitor-intent"
|
||||
litellm_params:
|
||||
guardrail: litellm_content_filter
|
||||
mode: pre_call
|
||||
default_on: false
|
||||
|
||||
# Generic inputs (customer provides)
|
||||
competitor_intent_config:
|
||||
# Your brand (Emirates) and how users refer to it
|
||||
brand_self:
|
||||
- emirates
|
||||
- ek
|
||||
|
||||
# Direct competitors (canonical names; use competitor_aliases for codes/typos)
|
||||
competitors:
|
||||
- qatar airways
|
||||
- qatar
|
||||
- etihad
|
||||
- turkish airlines
|
||||
- singapore airlines
|
||||
- british airways
|
||||
|
||||
# Geo entities: countries, cities, airports (for other-meaning vs airline disambiguation)
|
||||
locations:
|
||||
- qatar
|
||||
- doha
|
||||
- doh
|
||||
- hamad international
|
||||
- lusail
|
||||
|
||||
# Industry: airline domain words (trigger category_ranking / gate)
|
||||
domain_words:
|
||||
- airline
|
||||
- airlines
|
||||
- carrier
|
||||
- flight
|
||||
- flights
|
||||
- business class
|
||||
- first class
|
||||
- lounge
|
||||
- lounges
|
||||
- premium cabin
|
||||
- long haul
|
||||
|
||||
# Industry: route/geo cues (Gulf, hubs)
|
||||
route_geo_cues:
|
||||
- doha
|
||||
- dubai
|
||||
- abu dhabi
|
||||
- gulf
|
||||
- middle east
|
||||
|
||||
# Indirect references (descriptor lexicon)
|
||||
descriptor_lexicon:
|
||||
- doha airline
|
||||
- oryx airline
|
||||
- gulf carrier
|
||||
- five star airline
|
||||
- skytrax
|
||||
|
||||
# Per-competitor aliases (IATA codes, nicknames)
|
||||
competitor_aliases:
|
||||
qatar airways:
|
||||
- qr
|
||||
- doha airline
|
||||
qatar:
|
||||
- qr
|
||||
etihad:
|
||||
- ey
|
||||
- abu dhabi airline
|
||||
emirates:
|
||||
- ek
|
||||
- dubai airline
|
||||
|
||||
# Optional: regex patterns for indirect competitor mention (on normalized text)
|
||||
# indirect_competitor_patterns:
|
||||
# qatar airways:
|
||||
# - "the doha airline"
|
||||
# - "doha carrier"
|
||||
# etihad:
|
||||
# - "abu dhabi carrier"
|
||||
|
||||
# Policy: what to do per intent band
|
||||
policy:
|
||||
competitor_comparison: refuse
|
||||
possible_competitor_comparison: reframe
|
||||
category_ranking: reframe
|
||||
log_only: log_only
|
||||
|
||||
threshold_high: 0.70
|
||||
threshold_medium: 0.45
|
||||
threshold_low: 0.30
|
||||
|
||||
# Optional: custom messages
|
||||
reframe_message_template: >
|
||||
I'm here to help with Emirates services, bookings, and travel information.
|
||||
Would you like to know about our routes, cabin classes, or loyalty program?
|
||||
refuse_message_template: "We can't compare Emirates with other airlines. How can I help with Emirates travel?"
|
||||
|
||||
# --- How to use in config.yaml ---
|
||||
#
|
||||
# guardrails:
|
||||
# - guardrail_name: "emirates-competitor-intent"
|
||||
# litellm_params:
|
||||
# guardrail: litellm_content_filter
|
||||
# mode: pre_call
|
||||
# competitor_intent_config:
|
||||
# brand_self: [emirates, ek]
|
||||
# competitors: [qatar airways, etihad, ...]
|
||||
# domain_words: [airline, carrier, flight, ...]
|
||||
# route_geo_cues: [doha, dubai, abu dhabi]
|
||||
# descriptor_lexicon: [doha airline, gulf carrier, ...]
|
||||
# competitor_aliases:
|
||||
# qatar airways: [qr, doha airline]
|
||||
# etihad: [ey]
|
||||
# policy:
|
||||
# competitor_comparison: refuse
|
||||
# possible_competitor_comparison: reframe
|
||||
# category_ranking: reframe
|
||||
# log_only: log_only
|
||||
|
|
@ -115,6 +115,46 @@ class TestAirlineCompetitorIntentChecker:
|
|||
class TestContentFilterWithCompetitorIntent:
|
||||
"""Integration: ContentFilterGuardrail with competitor_intent_config."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_competitor_intent_type_airline_uses_airline_checker(self):
|
||||
"""When competitor_intent_type is airline (default), use AirlineCompetitorIntentChecker."""
|
||||
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import \
|
||||
ContentFilterGuardrail
|
||||
|
||||
guardrail = ContentFilterGuardrail(
|
||||
guardrail_name="test-airline",
|
||||
competitor_intent_config={
|
||||
"competitor_intent_type": "airline",
|
||||
"brand_self": ["emirates", "ek"],
|
||||
"locations": ["qatar", "doha"],
|
||||
"policy": {"competitor_comparison": "refuse"},
|
||||
},
|
||||
)
|
||||
assert guardrail._competitor_intent_checker is not None
|
||||
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.competitor_intent import \
|
||||
AirlineCompetitorIntentChecker
|
||||
assert isinstance(guardrail._competitor_intent_checker, AirlineCompetitorIntentChecker)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_competitor_intent_type_generic_uses_base_checker(self):
|
||||
"""When competitor_intent_type is generic, use BaseCompetitorIntentChecker."""
|
||||
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.competitor_intent import \
|
||||
BaseCompetitorIntentChecker
|
||||
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import \
|
||||
ContentFilterGuardrail
|
||||
|
||||
guardrail = ContentFilterGuardrail(
|
||||
guardrail_name="test-generic",
|
||||
competitor_intent_config={
|
||||
"competitor_intent_type": "generic",
|
||||
"brand_self": ["acme"],
|
||||
"competitors": ["widget inc", "gadget corp"],
|
||||
"policy": {"competitor_comparison": "refuse"},
|
||||
},
|
||||
)
|
||||
assert guardrail._competitor_intent_checker is not None
|
||||
assert isinstance(guardrail._competitor_intent_checker, BaseCompetitorIntentChecker)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_guardrail_with_competitor_intent_allow(self):
|
||||
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import \
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
const [blockedWords, setBlockedWords] = useState<any[]>([]);
|
||||
const [selectedContentCategories, setSelectedContentCategories] = useState<any[]>([]);
|
||||
const [pendingCategorySelection, setPendingCategorySelection] = useState<string>("");
|
||||
const [competitorIntentEnabled, setCompetitorIntentEnabled] = useState(false);
|
||||
const [competitorIntentConfig, setCompetitorIntentConfig] = useState<any>(null);
|
||||
const [toolPermissionConfig, setToolPermissionConfig] = useState<ToolPermissionConfig>({
|
||||
rules: [],
|
||||
default_action: "deny",
|
||||
|
|
@ -175,6 +177,8 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
setBlockedWords([]);
|
||||
setSelectedContentCategories([]);
|
||||
setPendingCategorySelection("");
|
||||
setCompetitorIntentEnabled(false);
|
||||
setCompetitorIntentConfig(null);
|
||||
|
||||
setToolPermissionConfig({
|
||||
rules: [],
|
||||
|
|
@ -254,7 +258,13 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
setCurrentStep(currentStep - 1);
|
||||
};
|
||||
|
||||
const handleAddAndContinue = () => {
|
||||
const handleAddAndContinue = (competitorIntentOnly?: boolean) => {
|
||||
// Competitor intent only: just advance to next step (no category to add)
|
||||
if (competitorIntentOnly) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pendingCategorySelection || !guardrailSettings) return;
|
||||
|
||||
const contentFilterSettings = guardrailSettings.content_filter_settings;
|
||||
|
|
@ -363,12 +373,20 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
}
|
||||
}
|
||||
|
||||
// For Content Filter, add patterns, blocked words, and categories
|
||||
// For Content Filter, add patterns, blocked words, categories, and optionally competitor intent
|
||||
if (shouldRenderContentFilterConfigSettings(values.provider)) {
|
||||
// Validate that at least one content filter setting is configured
|
||||
if (selectedPatterns.length === 0 && blockedWords.length === 0 && selectedContentCategories.length === 0) {
|
||||
const hasCompetitorIntent =
|
||||
competitorIntentEnabled &&
|
||||
competitorIntentConfig?.brand_self?.length > 0;
|
||||
if (
|
||||
selectedPatterns.length === 0 &&
|
||||
blockedWords.length === 0 &&
|
||||
selectedContentCategories.length === 0 &&
|
||||
!hasCompetitorIntent
|
||||
) {
|
||||
NotificationsManager.fromBackend(
|
||||
"Please configure at least one content filter setting (category, pattern, or keyword)"
|
||||
"Please configure at least one content filter setting (category, pattern, keyword, or competitor intent)"
|
||||
);
|
||||
setLoading(false);
|
||||
return;
|
||||
|
|
@ -398,6 +416,25 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
severity_threshold: c.severity_threshold || "medium",
|
||||
}));
|
||||
}
|
||||
if (competitorIntentEnabled && competitorIntentConfig?.brand_self?.length > 0) {
|
||||
guardrailData.litellm_params.competitor_intent_config = {
|
||||
competitor_intent_type: competitorIntentConfig.competitor_intent_type ?? "airline",
|
||||
brand_self: competitorIntentConfig.brand_self,
|
||||
locations:
|
||||
competitorIntentConfig.locations?.length > 0
|
||||
? competitorIntentConfig.locations
|
||||
: undefined,
|
||||
competitors:
|
||||
competitorIntentConfig.competitor_intent_type === "generic" &&
|
||||
competitorIntentConfig.competitors?.length > 0
|
||||
? competitorIntentConfig.competitors
|
||||
: undefined,
|
||||
policy: competitorIntentConfig.policy,
|
||||
threshold_high: competitorIntentConfig.threshold_high,
|
||||
threshold_medium: competitorIntentConfig.threshold_medium,
|
||||
threshold_low: competitorIntentConfig.threshold_low,
|
||||
};
|
||||
}
|
||||
}
|
||||
// Add config values to the guardrail_info if provided
|
||||
else if (values.config) {
|
||||
|
|
@ -712,6 +749,12 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
onPendingCategorySelectionChange={setPendingCategorySelection}
|
||||
accessToken={accessToken}
|
||||
showStep={step}
|
||||
competitorIntentEnabled={competitorIntentEnabled}
|
||||
competitorIntentConfig={competitorIntentConfig}
|
||||
onCompetitorIntentChange={(enabled, config) => {
|
||||
setCompetitorIntentEnabled(enabled);
|
||||
setCompetitorIntentConfig(config);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -774,6 +817,9 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
const isLastStep = currentStep === totalSteps - 1;
|
||||
const isCategoriesStep = shouldRenderContentFilterConfigSettings(selectedProvider) && currentStep === 1;
|
||||
const hasPendingCategory = pendingCategorySelection !== "";
|
||||
const hasCompetitorIntentConfigured =
|
||||
competitorIntentEnabled && (competitorIntentConfig?.brand_self?.length ?? 0) > 0;
|
||||
const canContinueFromCategoriesStep = hasPendingCategory || hasCompetitorIntentConfigured;
|
||||
|
||||
return (
|
||||
<div className="flex justify-end space-x-2 mt-4">
|
||||
|
|
@ -787,12 +833,12 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
<Button onClick={nextStep}>
|
||||
Skip
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleAddAndContinue}
|
||||
disabled={!hasPendingCategory}
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => handleAddAndContinue(hasCompetitorIntentConfigured)}
|
||||
disabled={!canContinueFromCategoriesStep}
|
||||
>
|
||||
Add & Continue →
|
||||
{hasPendingCategory ? "Add & Continue →" : "Continue →"}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,335 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
Typography,
|
||||
Select,
|
||||
Switch,
|
||||
Form,
|
||||
Space,
|
||||
InputNumber,
|
||||
} from "antd";
|
||||
import { getMajorAirlines } from "../../networking";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
export interface MajorAirline {
|
||||
id: string;
|
||||
match: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface CompetitorIntentConfig {
|
||||
competitor_intent_type: "airline" | "generic";
|
||||
brand_self: string[];
|
||||
locations?: string[];
|
||||
competitors?: string[];
|
||||
policy?: {
|
||||
competitor_comparison?: "refuse" | "reframe";
|
||||
possible_competitor_comparison?: "refuse" | "reframe";
|
||||
};
|
||||
threshold_high?: number;
|
||||
threshold_medium?: number;
|
||||
threshold_low?: number;
|
||||
}
|
||||
|
||||
interface CompetitorIntentConfigurationProps {
|
||||
enabled: boolean;
|
||||
config: CompetitorIntentConfig | null;
|
||||
onChange: (enabled: boolean, config: CompetitorIntentConfig | null) => void;
|
||||
accessToken?: string | null;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: CompetitorIntentConfig = {
|
||||
competitor_intent_type: "airline",
|
||||
brand_self: [],
|
||||
locations: [],
|
||||
policy: {
|
||||
competitor_comparison: "refuse",
|
||||
possible_competitor_comparison: "reframe",
|
||||
},
|
||||
threshold_high: 0.7,
|
||||
threshold_medium: 0.45,
|
||||
threshold_low: 0.3,
|
||||
};
|
||||
|
||||
const CompetitorIntentConfiguration: React.FC<
|
||||
CompetitorIntentConfigurationProps
|
||||
> = ({ enabled, config, onChange, accessToken }) => {
|
||||
const effectiveConfig = config ?? DEFAULT_CONFIG;
|
||||
const [airlineOptions, setAirlineOptions] = useState<MajorAirline[]>([]);
|
||||
const [loadingAirlines, setLoadingAirlines] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
effectiveConfig.competitor_intent_type === "airline" &&
|
||||
accessToken &&
|
||||
airlineOptions.length === 0
|
||||
) {
|
||||
setLoadingAirlines(true);
|
||||
getMajorAirlines(accessToken)
|
||||
.then((res) => setAirlineOptions(res.airlines ?? []))
|
||||
.catch(() => setAirlineOptions([]))
|
||||
.finally(() => setLoadingAirlines(false));
|
||||
}
|
||||
}, [effectiveConfig.competitor_intent_type, accessToken, airlineOptions.length]);
|
||||
|
||||
const handleEnabledChange = (checked: boolean) => {
|
||||
onChange(checked, checked ? { ...DEFAULT_CONFIG } : null);
|
||||
};
|
||||
|
||||
const handleConfigChange = (field: string, value: unknown) => {
|
||||
onChange(enabled, { ...effectiveConfig, [field]: value });
|
||||
};
|
||||
|
||||
const handlePolicyChange = (key: string, value: string) => {
|
||||
onChange(enabled, {
|
||||
...effectiveConfig,
|
||||
policy: { ...effectiveConfig.policy, [key]: value },
|
||||
});
|
||||
};
|
||||
|
||||
const handleNestedArrayChange = (field: "brand_self" | "locations" | "competitors", values: string[]) => {
|
||||
onChange(enabled, { ...effectiveConfig, [field]: values.filter(Boolean) });
|
||||
};
|
||||
|
||||
const handleBrandSelfChange = (values: string[]) => {
|
||||
const filtered = values.filter(Boolean);
|
||||
const expanded: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const v of filtered) {
|
||||
const airline = airlineOptions.find((a) => {
|
||||
const primary = a.match.split("|")[0]?.trim().toLowerCase();
|
||||
return primary === v.toLowerCase();
|
||||
});
|
||||
if (airline) {
|
||||
for (const variant of airline.match.split("|").map((s) => s.trim().toLowerCase()).filter(Boolean)) {
|
||||
if (!seen.has(variant)) {
|
||||
seen.add(variant);
|
||||
expanded.push(variant);
|
||||
}
|
||||
}
|
||||
} else if (!seen.has(v.toLowerCase())) {
|
||||
seen.add(v.toLowerCase());
|
||||
expanded.push(v);
|
||||
}
|
||||
}
|
||||
onChange(enabled, { ...effectiveConfig, brand_self: expanded });
|
||||
};
|
||||
|
||||
|
||||
if (!enabled) {
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Title level={5} style={{ margin: 0 }}>
|
||||
Competitor Intent Filter
|
||||
</Title>
|
||||
<Switch checked={false} onChange={handleEnabledChange} />
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
>
|
||||
<Text type="secondary">
|
||||
Block or reframe competitor comparison questions. When enabled, airline type
|
||||
auto-loads competitors from IATA; generic type requires manual competitor list.
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Title level={5} style={{ margin: 0 }}>
|
||||
Competitor Intent Filter
|
||||
</Title>
|
||||
<Switch checked={enabled} onChange={handleEnabledChange} />
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
>
|
||||
<Text type="secondary" style={{ display: "block", marginBottom: 16 }}>
|
||||
Block or reframe competitor comparison questions. Airline type uses major airlines
|
||||
(excluding your brand); generic requires manual competitor list.
|
||||
</Text>
|
||||
<Form layout="vertical" size="small">
|
||||
<Form.Item label="Type">
|
||||
<Select
|
||||
value={effectiveConfig.competitor_intent_type}
|
||||
onChange={(v) => handleConfigChange("competitor_intent_type", v)}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Option value="airline">Airline (auto-load competitors from IATA)</Option>
|
||||
<Option value="generic">Generic (specify competitors manually)</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Your Brand (brand_self)"
|
||||
required
|
||||
help={
|
||||
effectiveConfig.competitor_intent_type === "airline"
|
||||
? "Select your airline from the list (excluded from competitors) or type to add a custom term"
|
||||
: "Names/codes users use for your brand"
|
||||
}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder={
|
||||
loadingAirlines
|
||||
? "Loading airlines..."
|
||||
: effectiveConfig.competitor_intent_type === "airline"
|
||||
? "Search or select airline, or type to add custom"
|
||||
: "Type and press Enter to add"
|
||||
}
|
||||
value={effectiveConfig.brand_self}
|
||||
onChange={(v) =>
|
||||
effectiveConfig.competitor_intent_type === "airline" && airlineOptions.length > 0
|
||||
? handleBrandSelfChange(v ?? [])
|
||||
: handleNestedArrayChange("brand_self", v ?? [])
|
||||
}
|
||||
tokenSeparators={[","]}
|
||||
loading={loadingAirlines}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label?.toString().toLowerCase() ?? "").includes(
|
||||
input.toLowerCase()
|
||||
)
|
||||
}
|
||||
optionFilterProp="label"
|
||||
options={
|
||||
effectiveConfig.competitor_intent_type === "airline" && airlineOptions.length > 0
|
||||
? airlineOptions.map((a) => {
|
||||
const primary = a.match.split("|")[0]?.trim() ?? a.id;
|
||||
const variants = a.match
|
||||
.split("|")
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
return {
|
||||
value: primary.toLowerCase(),
|
||||
label: `${primary}${variants.length > 1 ? ` (${variants.slice(1).join(", ")})` : ""}`,
|
||||
};
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{effectiveConfig.competitor_intent_type === "airline" && (
|
||||
<Form.Item
|
||||
label="Locations (optional)"
|
||||
help="Countries, cities, airports for disambiguation (e.g. qatar, doha)"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Type and press Enter to add"
|
||||
value={effectiveConfig.locations ?? []}
|
||||
onChange={(v) => handleNestedArrayChange("locations", v ?? [])}
|
||||
tokenSeparators={[","]}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{effectiveConfig.competitor_intent_type === "generic" && (
|
||||
<Form.Item
|
||||
label="Competitors"
|
||||
required
|
||||
help="Competitor names to detect (required for generic type)"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Type and press Enter to add"
|
||||
value={effectiveConfig.competitors ?? []}
|
||||
onChange={(v) => handleNestedArrayChange("competitors", v ?? [])}
|
||||
tokenSeparators={[","]}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item label="Policy: Competitor comparison">
|
||||
<Select
|
||||
value={effectiveConfig.policy?.competitor_comparison ?? "refuse"}
|
||||
onChange={(v) => handlePolicyChange("competitor_comparison", v)}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Option value="refuse">Refuse (block request)</Option>
|
||||
<Option value="reframe">Reframe (suggest alternative)</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Policy: Possible competitor comparison">
|
||||
<Select
|
||||
value={effectiveConfig.policy?.possible_competitor_comparison ?? "reframe"}
|
||||
onChange={(v) => handlePolicyChange("possible_competitor_comparison", v)}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Option value="refuse">Refuse (block request)</Option>
|
||||
<Option value="reframe">Reframe (suggest alternative to backend LLM)</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Confidence thresholds"
|
||||
help={
|
||||
<>
|
||||
Classify competitor intent by confidence (0–1). Higher confidence → stronger intent.
|
||||
<ul style={{ marginBottom: 0, marginTop: 4, paddingLeft: 20 }}>
|
||||
<li>
|
||||
<strong>High (≥)</strong>: Treat as full competitor comparison → uses "Competitor comparison" policy
|
||||
</li>
|
||||
<li>
|
||||
<strong>Medium (≥)</strong>: Treat as possible comparison → uses "Possible competitor comparison" policy
|
||||
</li>
|
||||
<li>
|
||||
<strong>Low (≥)</strong>: Log only; allow request. Below Low → allow with no action
|
||||
</li>
|
||||
</ul>
|
||||
Raise thresholds to be more permissive; lower them to be stricter.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Space wrap>
|
||||
<Form.Item label="High" style={{ marginBottom: 0 }} help="e.g. 0.7">
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={effectiveConfig.threshold_high ?? 0.7}
|
||||
onChange={(v) => handleConfigChange("threshold_high", v ?? 0.7)}
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Medium" style={{ marginBottom: 0 }} help="e.g. 0.45">
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={effectiveConfig.threshold_medium ?? 0.45}
|
||||
onChange={(v) => handleConfigChange("threshold_medium", v ?? 0.45)}
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Low" style={{ marginBottom: 0 }} help="e.g. 0.3">
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={effectiveConfig.threshold_low ?? 0.3}
|
||||
onChange={(v) => handleConfigChange("threshold_low", v ?? 0.3)}
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompetitorIntentConfiguration;
|
||||
|
|
@ -9,6 +9,9 @@ import KeywordModal from "./KeywordModal";
|
|||
import PatternTable from "./PatternTable";
|
||||
import KeywordTable from "./KeywordTable";
|
||||
import ContentCategoryConfiguration from "./ContentCategoryConfiguration";
|
||||
import CompetitorIntentConfiguration, {
|
||||
CompetitorIntentConfig,
|
||||
} from "./CompetitorIntentConfiguration";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
|
|
@ -63,7 +66,7 @@ interface ContentFilterConfigurationProps {
|
|||
onBlockedWordUpdate: (id: string, field: string, value: any) => void;
|
||||
onFileUpload?: (content: string) => void;
|
||||
accessToken: string | null;
|
||||
showStep?: "patterns" | "keywords" | "categories";
|
||||
showStep?: "patterns" | "keywords" | "categories" | "competitor_intent";
|
||||
contentCategories?: ContentCategory[];
|
||||
selectedContentCategories?: SelectedContentCategory[];
|
||||
onContentCategoryAdd?: (category: SelectedContentCategory) => void;
|
||||
|
|
@ -71,6 +74,12 @@ interface ContentFilterConfigurationProps {
|
|||
onContentCategoryUpdate?: (id: string, field: string, value: any) => void;
|
||||
pendingCategorySelection?: string;
|
||||
onPendingCategorySelectionChange?: (value: string) => void;
|
||||
competitorIntentEnabled?: boolean;
|
||||
competitorIntentConfig?: CompetitorIntentConfig | null;
|
||||
onCompetitorIntentChange?: (
|
||||
enabled: boolean,
|
||||
config: CompetitorIntentConfig | null
|
||||
) => void;
|
||||
}
|
||||
|
||||
const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
|
||||
|
|
@ -94,6 +103,9 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
|
|||
onContentCategoryUpdate,
|
||||
pendingCategorySelection,
|
||||
onPendingCategorySelectionChange,
|
||||
competitorIntentEnabled = false,
|
||||
competitorIntentConfig = null,
|
||||
onCompetitorIntentChange,
|
||||
}) => {
|
||||
const [patternModalVisible, setPatternModalVisible] = useState(false);
|
||||
const [keywordModalVisible, setKeywordModalVisible] = useState(false);
|
||||
|
|
@ -197,6 +209,8 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
|
|||
const showPatterns = !showStep || showStep === "patterns";
|
||||
const showKeywords = !showStep || showStep === "keywords";
|
||||
const showCategories = !showStep || showStep === "categories";
|
||||
const showCompetitorIntent =
|
||||
!showStep || showStep === "competitor_intent" || showStep === "categories";
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
|
@ -274,6 +288,16 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
|
|||
</Card>
|
||||
)}
|
||||
|
||||
{showCompetitorIntent &&
|
||||
onCompetitorIntentChange && (
|
||||
<CompetitorIntentConfiguration
|
||||
enabled={competitorIntentEnabled}
|
||||
config={competitorIntentConfig}
|
||||
onChange={onCompetitorIntentChange}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showCategories && contentCategories.length > 0 && onContentCategoryAdd && onContentCategoryRemove && onContentCategoryUpdate && (
|
||||
<ContentCategoryConfiguration
|
||||
availableCategories={contentCategories}
|
||||
|
|
|
|||
|
|
@ -384,7 +384,7 @@ describe("ContentFilterManager", () => {
|
|||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnDataChange).toHaveBeenCalledWith([], [], []);
|
||||
expect(mockOnDataChange).toHaveBeenCalledWith([], [], [], false, null);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Alert, Divider, Typography } from "antd";
|
|||
import React, { useEffect, useState } from "react";
|
||||
import ContentFilterConfiguration from "./ContentFilterConfiguration";
|
||||
import ContentFilterDisplay from "./ContentFilterDisplay";
|
||||
import type { CompetitorIntentConfig } from "./CompetitorIntentConfiguration";
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
|
|
@ -55,7 +56,13 @@ interface ContentFilterManagerProps {
|
|||
guardrailSettings: GuardrailSettings | null;
|
||||
isEditing: boolean;
|
||||
accessToken: string | null;
|
||||
onDataChange?: (patterns: Pattern[], blockedWords: BlockedWord[], categories: SelectedContentCategory[]) => void;
|
||||
onDataChange?: (
|
||||
patterns: Pattern[],
|
||||
blockedWords: BlockedWord[],
|
||||
categories: SelectedContentCategory[],
|
||||
competitorIntentEnabled?: boolean,
|
||||
competitorIntentConfig?: CompetitorIntentConfig | null
|
||||
) => void;
|
||||
onUnsavedChanges?: (hasChanges: boolean) => void;
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +80,10 @@ const ContentFilterManager: React.FC<ContentFilterManagerProps> = ({
|
|||
const [originalPatterns, setOriginalPatterns] = useState<Pattern[]>([]);
|
||||
const [originalBlockedWords, setOriginalBlockedWords] = useState<BlockedWord[]>([]);
|
||||
const [originalContentCategories, setOriginalContentCategories] = useState<SelectedContentCategory[]>([]);
|
||||
const [competitorIntentEnabled, setCompetitorIntentEnabled] = useState(false);
|
||||
const [competitorIntentConfig, setCompetitorIntentConfig] = useState<CompetitorIntentConfig | null>(null);
|
||||
const [originalCompetitorIntentEnabled, setOriginalCompetitorIntentEnabled] = useState(false);
|
||||
const [originalCompetitorIntentConfig, setOriginalCompetitorIntentConfig] = useState<CompetitorIntentConfig | null>(null);
|
||||
|
||||
// Load data from guardrail on mount or when guardrailData changes
|
||||
useEffect(() => {
|
||||
|
|
@ -128,22 +139,73 @@ const ContentFilterManager: React.FC<ContentFilterManagerProps> = ({
|
|||
setSelectedContentCategories([]);
|
||||
setOriginalContentCategories([]);
|
||||
}
|
||||
|
||||
const cic = guardrailData?.litellm_params?.competitor_intent_config;
|
||||
if (cic && typeof cic === "object") {
|
||||
const enabled = !!(cic.brand_self && Array.isArray(cic.brand_self) && cic.brand_self.length > 0);
|
||||
const config: CompetitorIntentConfig = {
|
||||
competitor_intent_type: cic.competitor_intent_type ?? "airline",
|
||||
brand_self: Array.isArray(cic.brand_self) ? cic.brand_self : [],
|
||||
locations: Array.isArray(cic.locations) ? cic.locations : [],
|
||||
competitors: Array.isArray(cic.competitors) ? cic.competitors : [],
|
||||
policy: cic.policy ?? { competitor_comparison: "refuse", possible_competitor_comparison: "reframe" },
|
||||
threshold_high: typeof cic.threshold_high === "number" ? cic.threshold_high : 0.7,
|
||||
threshold_medium: typeof cic.threshold_medium === "number" ? cic.threshold_medium : 0.45,
|
||||
threshold_low: typeof cic.threshold_low === "number" ? cic.threshold_low : 0.3,
|
||||
};
|
||||
setCompetitorIntentEnabled(enabled);
|
||||
setCompetitorIntentConfig(config);
|
||||
setOriginalCompetitorIntentEnabled(enabled);
|
||||
setOriginalCompetitorIntentConfig(config);
|
||||
} else {
|
||||
setCompetitorIntentEnabled(false);
|
||||
setCompetitorIntentConfig(null);
|
||||
setOriginalCompetitorIntentEnabled(false);
|
||||
setOriginalCompetitorIntentConfig(null);
|
||||
}
|
||||
}, [guardrailData, guardrailSettings?.content_filter_settings?.content_categories]);
|
||||
|
||||
// Notify parent component when data changes
|
||||
useEffect(() => {
|
||||
if (onDataChange) {
|
||||
onDataChange(selectedPatterns, blockedWords, selectedContentCategories);
|
||||
onDataChange(
|
||||
selectedPatterns,
|
||||
blockedWords,
|
||||
selectedContentCategories,
|
||||
competitorIntentEnabled,
|
||||
competitorIntentConfig
|
||||
);
|
||||
}
|
||||
}, [selectedPatterns, blockedWords, selectedContentCategories, onDataChange]);
|
||||
}, [
|
||||
selectedPatterns,
|
||||
blockedWords,
|
||||
selectedContentCategories,
|
||||
competitorIntentEnabled,
|
||||
competitorIntentConfig,
|
||||
onDataChange,
|
||||
]);
|
||||
|
||||
// Detect unsaved changes
|
||||
const hasUnsavedChanges = React.useMemo(() => {
|
||||
const hasPatternChanges = JSON.stringify(selectedPatterns) !== JSON.stringify(originalPatterns);
|
||||
const hasWordChanges = JSON.stringify(blockedWords) !== JSON.stringify(originalBlockedWords);
|
||||
const hasCategoryChanges = JSON.stringify(selectedContentCategories) !== JSON.stringify(originalContentCategories);
|
||||
return hasPatternChanges || hasWordChanges || hasCategoryChanges;
|
||||
}, [selectedPatterns, blockedWords, selectedContentCategories, originalPatterns, originalBlockedWords, originalContentCategories]);
|
||||
const hasCompetitorIntentChanges =
|
||||
competitorIntentEnabled !== originalCompetitorIntentEnabled ||
|
||||
JSON.stringify(competitorIntentConfig) !== JSON.stringify(originalCompetitorIntentConfig);
|
||||
return hasPatternChanges || hasWordChanges || hasCategoryChanges || hasCompetitorIntentChanges;
|
||||
}, [
|
||||
selectedPatterns,
|
||||
blockedWords,
|
||||
selectedContentCategories,
|
||||
competitorIntentEnabled,
|
||||
competitorIntentConfig,
|
||||
originalPatterns,
|
||||
originalBlockedWords,
|
||||
originalContentCategories,
|
||||
originalCompetitorIntentEnabled,
|
||||
originalCompetitorIntentConfig,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && onUnsavedChanges) {
|
||||
|
|
@ -219,6 +281,12 @@ const ContentFilterManager: React.FC<ContentFilterManagerProps> = ({
|
|||
selectedContentCategories.map((c) => (c.id === id ? { ...c, [field]: value } : c))
|
||||
)
|
||||
}
|
||||
competitorIntentEnabled={competitorIntentEnabled}
|
||||
competitorIntentConfig={competitorIntentConfig}
|
||||
onCompetitorIntentChange={(enabled, config) => {
|
||||
setCompetitorIntentEnabled(enabled);
|
||||
setCompetitorIntentConfig(config);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -232,12 +300,15 @@ export default ContentFilterManager;
|
|||
export const formatContentFilterDataForAPI = (
|
||||
patterns: Pattern[],
|
||||
blockedWords: BlockedWord[],
|
||||
categories?: SelectedContentCategory[]
|
||||
categories?: SelectedContentCategory[],
|
||||
competitorIntentEnabled?: boolean,
|
||||
competitorIntentConfig?: CompetitorIntentConfig | null
|
||||
) => {
|
||||
const result: {
|
||||
patterns: any[];
|
||||
blocked_words: any[];
|
||||
categories?: any[];
|
||||
competitor_intent_config?: any;
|
||||
} = {
|
||||
patterns: patterns.map((p) => ({
|
||||
pattern_type: p.type === "prebuilt" ? "prebuilt" : "regex",
|
||||
|
|
@ -260,5 +331,23 @@ export const formatContentFilterDataForAPI = (
|
|||
severity_threshold: c.severity_threshold || "medium",
|
||||
}));
|
||||
}
|
||||
if (competitorIntentEnabled && competitorIntentConfig && competitorIntentConfig.brand_self.length > 0) {
|
||||
result.competitor_intent_config = {
|
||||
competitor_intent_type: competitorIntentConfig.competitor_intent_type,
|
||||
brand_self: competitorIntentConfig.brand_self,
|
||||
locations: competitorIntentConfig.locations?.length
|
||||
? competitorIntentConfig.locations
|
||||
: undefined,
|
||||
competitors:
|
||||
competitorIntentConfig.competitor_intent_type === "generic" &&
|
||||
competitorIntentConfig.competitors?.length
|
||||
? competitorIntentConfig.competitors
|
||||
: undefined,
|
||||
policy: competitorIntentConfig.policy,
|
||||
threshold_high: competitorIntentConfig.threshold_high,
|
||||
threshold_medium: competitorIntentConfig.threshold_medium,
|
||||
threshold_low: competitorIntentConfig.threshold_low,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -106,6 +106,8 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
patterns: any[];
|
||||
blockedWords: any[];
|
||||
categories: any[];
|
||||
competitorIntentEnabled?: boolean;
|
||||
competitorIntentConfig?: any;
|
||||
}>({
|
||||
patterns: [],
|
||||
blockedWords: [],
|
||||
|
|
@ -113,9 +115,24 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
});
|
||||
|
||||
// Memoize onDataChange callback to prevent unnecessary re-renders
|
||||
const handleContentFilterDataChange = useCallback((patterns: any[], blockedWords: any[], categories: any[]) => {
|
||||
contentFilterDataRef.current = { patterns, blockedWords, categories: categories || [] };
|
||||
}, []);
|
||||
const handleContentFilterDataChange = useCallback(
|
||||
(
|
||||
patterns: any[],
|
||||
blockedWords: any[],
|
||||
categories: any[],
|
||||
competitorIntentEnabled?: boolean,
|
||||
competitorIntentConfig?: any
|
||||
) => {
|
||||
contentFilterDataRef.current = {
|
||||
patterns,
|
||||
blockedWords,
|
||||
categories: categories || [],
|
||||
competitorIntentEnabled,
|
||||
competitorIntentConfig,
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const fetchGuardrailInfo = async () => {
|
||||
try {
|
||||
|
|
@ -287,11 +304,15 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
contentFilterDataRef.current.patterns || [],
|
||||
contentFilterDataRef.current.blockedWords || [],
|
||||
contentFilterDataRef.current.categories || [],
|
||||
contentFilterDataRef.current.competitorIntentEnabled,
|
||||
contentFilterDataRef.current.competitorIntentConfig
|
||||
);
|
||||
|
||||
updateData.litellm_params.patterns = formattedData.patterns;
|
||||
updateData.litellm_params.blocked_words = formattedData.blocked_words;
|
||||
updateData.litellm_params.categories = formattedData.categories;
|
||||
updateData.litellm_params.competitor_intent_config =
|
||||
formattedData.competitor_intent_config ?? null;
|
||||
}
|
||||
|
||||
if (guardrailData.litellm_params?.guardrail === "tool_permission") {
|
||||
|
|
|
|||
|
|
@ -7817,6 +7817,38 @@ export const getCategoryYaml = async (accessToken: string, categoryName: string)
|
|||
}
|
||||
};
|
||||
|
||||
export const getMajorAirlines = async (accessToken: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/guardrails/ui/major_airlines`
|
||||
: `/guardrails/ui/major_airlines`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
console.error(
|
||||
`Failed to get major airlines. Status: ${response.status}, Error:`,
|
||||
errorData
|
||||
);
|
||||
handleError(errorData);
|
||||
throw new Error(`Failed to get major airlines: ${response.status} ${errorData}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to get major airlines:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getAgentsList = async (accessToken: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/agents` : `/v1/agents`;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue