diff --git a/application/external_apps/databaseseeder/artifacts/admin_settings.json b/application/external_apps/databaseseeder/artifacts/admin_settings.json index 24c1c7ea4..2bbd3c5a1 100644 --- a/application/external_apps/databaseseeder/artifacts/admin_settings.json +++ b/application/external_apps/databaseseeder/artifacts/admin_settings.json @@ -90,6 +90,8 @@ "audio_files_authentication_type": "key", "audio_files_key": "", "enable_content_safety": false, + "content_safety_violation_message": "Your message was blocked by Content Safety.", + "content_safety_include_trigger_information": true, "require_member_of_safety_violation_admin": false, "content_safety_endpoint": "", "content_safety_key": "", diff --git a/application/single_app/config.py b/application/single_app/config.py index 4c9b91a3e..a182549ff 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -97,7 +97,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.250.059" +VERSION = "0.250.061" IS_DEVELOPMENT = is_development_env_enabled() SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax') diff --git a/application/single_app/functions_content_safety.py b/application/single_app/functions_content_safety.py new file mode 100644 index 000000000..e44729fdf --- /dev/null +++ b/application/single_app/functions_content_safety.py @@ -0,0 +1,64 @@ +# functions_content_safety.py +"""Helpers for formatting user-visible Content Safety violation messages.""" + +CONTENT_SAFETY_VIOLATION_MESSAGE_DEFAULT = 'Your message was blocked by Content Safety.' +CONTENT_SAFETY_VIOLATION_MESSAGE_MAX_LENGTH = 3000 + + +def normalize_content_safety_violation_message(value): + """Return a non-empty, bounded Markdown message template.""" + candidate = str(value or '').replace('\r\n', '\n').replace('\r', '\n').strip() + if not candidate: + return CONTENT_SAFETY_VIOLATION_MESSAGE_DEFAULT + return candidate[:CONTENT_SAFETY_VIOLATION_MESSAGE_MAX_LENGTH] + + +def build_content_safety_violation_message( + settings, + block_reasons, + triggered_categories, + blocklist_matches, +): + """Build the Markdown message shown after a blocked chat request.""" + safety_settings = settings if isinstance(settings, dict) else {} + message_template = normalize_content_safety_violation_message( + safety_settings.get('content_safety_violation_message') + ) + + if safety_settings.get('content_safety_include_trigger_information', True) is False: + return message_template + + normalized_reasons = [ + str(reason).strip() + for reason in (block_reasons or []) + if str(reason or '').strip() + ] + trigger_lines = [ + f"**Reason**: {', '.join(normalized_reasons)}", + 'Triggered categories:', + ] + + for category in triggered_categories or []: + if not isinstance(category, dict): + continue + category_name = str(category.get('category') or '').strip() + if category_name: + trigger_lines.append( + f" - {category_name} (severity={category.get('severity')})" + ) + + formatted_blocklist_matches = [] + for match in blocklist_matches or []: + if not isinstance(match, dict): + continue + item_text = str(match.get('blocklistItemText') or '').strip() + blocklist_name = str(match.get('blocklistName') or '').strip() + if item_text and blocklist_name: + formatted_blocklist_matches.append( + f" - {item_text} (in {blocklist_name})" + ) + + if formatted_blocklist_matches: + trigger_lines.extend(['', 'Blocklist Matches:', *formatted_blocklist_matches]) + + return f"{message_template}\n\n" + '\n'.join(trigger_lines) diff --git a/application/single_app/functions_settings.py b/application/single_app/functions_settings.py index d54d3824f..9acbf63a9 100644 --- a/application/single_app/functions_settings.py +++ b/application/single_app/functions_settings.py @@ -6,6 +6,9 @@ from config import * from functions_appinsights import log_event +from functions_content_safety import ( + CONTENT_SAFETY_VIOLATION_MESSAGE_DEFAULT, +) from functions_cosmos_throughput import get_default_cosmos_throughput_settings from functions_document_actions import get_default_document_action_capabilities from functions_icon_utils import normalize_icon_payload @@ -1063,6 +1066,8 @@ def get_settings(use_cosmos=False, include_source=False): # Safety (Content Safety) Settings 'enable_content_safety': False, + 'content_safety_violation_message': CONTENT_SAFETY_VIOLATION_MESSAGE_DEFAULT, + 'content_safety_include_trigger_information': True, 'require_member_of_safety_violation_admin': False, 'require_member_of_control_center_admin': False, 'require_member_of_control_center_dashboard_reader': False, diff --git a/application/single_app/route_backend_chats.py b/application/single_app/route_backend_chats.py index e73a05c03..b7db677e4 100644 --- a/application/single_app/route_backend_chats.py +++ b/application/single_app/route_backend_chats.py @@ -59,6 +59,7 @@ SEMANTIC_SEARCH_QUOTA_WARNING_TYPE, SemanticSearchQuotaExceededError, ) +from functions_content_safety import build_content_safety_violation_message from functions_settings import * from functions_assigned_knowledge import ( ASSIGNED_KNOWLEDGE_USER_ACTION_ANALYZE, @@ -2707,7 +2708,6 @@ def _build_safety_message_doc( }, }) - def _build_fact_memory_context_lines( scope_id, scope_type, @@ -13410,21 +13410,12 @@ def result_requires_message_reload(result: Any) -> bool: cosmos_safety_container.upsert_item(safety_item) # Instead of 403, we'll add a "safety" message - blocked_msg_content = ( - "Your message was blocked by Content Safety.\n\n" - f"**Reason**: {', '.join(block_reasons)}\n" - "Triggered categories:\n" + blocked_msg_content = build_content_safety_violation_message( + settings=settings, + block_reasons=block_reasons, + triggered_categories=triggered_categories, + blocklist_matches=blocklist_matches, ) - for cat in triggered_categories: - blocked_msg_content += ( - f" - {cat['category']} (severity={cat['severity']})\n" - ) - if blocklist_matches: - blocked_msg_content += ( - "\nBlocklist Matches:\n" + - "\n".join([f" - {m['blocklistItemText']} (in {m['blocklistName']})" - for m in blocklist_matches]) - ) # Insert a special "role": "safety" or "blocked" safety_doc = _build_safety_message_doc( @@ -17039,21 +17030,12 @@ def record_and_publish_streaming_thought(thought_payload): cosmos_safety_container.upsert_item(safety_item) # Build blocked message - blocked_msg_content = ( - "Your message was blocked by Content Safety.\n\n" - f"**Reason**: {', '.join(block_reasons)}\n" - "Triggered categories:\n" + blocked_msg_content = build_content_safety_violation_message( + settings=settings, + block_reasons=block_reasons, + triggered_categories=triggered_categories, + blocklist_matches=blocklist_matches, ) - for cat in triggered_categories: - blocked_msg_content += ( - f" - {cat['category']} (severity={cat['severity']})\n" - ) - if blocklist_matches: - blocked_msg_content += ( - "\nBlocklist Matches:\n" + - "\n".join([f" - {m['blocklistItemText']} (in {m['blocklistName']})" - for m in blocklist_matches]) - ) # Insert safety message safety_doc = _build_safety_message_doc( diff --git a/application/single_app/route_frontend_admin_settings.py b/application/single_app/route_frontend_admin_settings.py index 897951520..878c2abd8 100644 --- a/application/single_app/route_frontend_admin_settings.py +++ b/application/single_app/route_frontend_admin_settings.py @@ -9,6 +9,7 @@ from functions_keyvault import keyvault_model_endpoint_cleanup_helper, keyvault_model_endpoint_delete_helper, keyvault_model_endpoint_save_helper, redact_model_endpoint_secret_values from functions_settings import * +from functions_content_safety import normalize_content_safety_violation_message from functions_file_sync import FILE_SYNC_DEFAULTS, get_file_sync_config from functions_source_review import SOURCE_REVIEW_DEFAULTS, get_source_review_config, get_source_review_runtime_capabilities, normalize_source_review_js_rendering_enabled, parse_source_review_list from functions_control_center import ( @@ -126,6 +127,7 @@ def normalize_agents_page_text(value, fallback, max_length): candidate = fallback return candidate[:max_length] + def register_route_frontend_admin_settings(bp): @bp.route('/admin/settings', methods=['GET', 'POST']) @swagger_route(security=get_auth_security()) @@ -720,6 +722,12 @@ def parse_admin_int(raw_value, fallback_value, field_name="unknown", hard_defaul file_download_allowed_public_workspace_ids = normalize_file_download_allowed_public_workspace_ids( form_data.get('file_download_allowed_public_workspace_ids', '') ) + content_safety_violation_message = normalize_content_safety_violation_message( + form_data.get('content_safety_violation_message') + ) + content_safety_include_trigger_information = form_data.get( + 'content_safety_include_trigger_information' + ) == 'on' require_member_of_safety_violation_admin = form_data.get('require_member_of_safety_violation_admin') == 'on' require_member_of_control_center_admin = form_data.get('require_member_of_control_center_admin') == 'on' require_member_of_control_center_dashboard_reader = form_data.get('require_member_of_control_center_dashboard_reader') == 'on' @@ -2105,6 +2113,8 @@ def is_valid_url(url): # Safety (Content Safety Direct & APIM) 'enable_content_safety': form_data.get('enable_content_safety') == 'on', + 'content_safety_violation_message': content_safety_violation_message, + 'content_safety_include_trigger_information': content_safety_include_trigger_information, 'content_safety_endpoint': form_data.get('content_safety_endpoint', '').strip(), 'content_safety_key': admin_secret('content_safety_key'), 'content_safety_authentication_type': form_data.get('content_safety_authentication_type', 'key'), diff --git a/application/single_app/templates/admin_settings.html b/application/single_app/templates/admin_settings.html index e9b758218..74d0fa978 100644 --- a/application/single_app/templates/admin_settings.html +++ b/application/single_app/templates/admin_settings.html @@ -9062,6 +9062,37 @@
Disable this option to show only the configured message.
+ @@ -11129,6 +11160,7 @@