Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.250.102"
VERSION = "0.250.103"
IS_DEVELOPMENT = is_development_env_enabled()

SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
Expand Down
37 changes: 37 additions & 0 deletions application/single_app/functions_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from flask import current_app
import logging
from config import cosmos_notifications_container
from functions_appinsights import log_event

Check warning on line 21 in application/single_app/functions_notifications.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
Comment thread
paullizer marked this conversation as resolved.
Dismissed
from functions_group import find_group_by_id
from functions_debug import debug_print
from functions_public_workspaces import find_public_workspace_by_id, get_user_public_workspaces
Expand Down Expand Up @@ -755,6 +756,42 @@
return 0


def get_recent_chat_response_notifications(user_id, limit=50):
"""Return recent personal chat completion event identities for one user."""
try:

Check warning on line 761 in application/single_app/functions_notifications.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
normalized_limit = max(1, min(int(limit or 50), 100))
except (TypeError, ValueError):
normalized_limit = 50

try:

Check warning on line 766 in application/single_app/functions_notifications.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
notifications = list(cosmos_notifications_container.query_items(

Check warning on line 767 in application/single_app/functions_notifications.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
query=(
f"SELECT TOP {normalized_limit} "
"c.id, c.created_at, c.link_context, c.metadata "
"FROM c WHERE c.user_id = @user_id "
"AND c.notification_type = @notification_type "
"ORDER BY c.created_at DESC"
),
parameters=[
{"name": "@user_id", "value": user_id},
{"name": "@notification_type", "value": "chat_response_complete"},
],
partition_key=user_id,
))
return notifications
except Exception as e:

Check warning on line 782 in application/single_app/functions_notifications.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
log_event(

Check warning on line 783 in application/single_app/functions_notifications.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
"[Notifications] Failed to load recent chat completion events.",
extra={
"user_id": user_id,
"error": str(e),
},
level=logging.ERROR,
exceptionTraceback=True,

Check warning on line 790 in application/single_app/functions_notifications.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
)
raise


def get_unread_workflow_priority_notifications(user_id, limit=5):
"""Return the most recent unread workflow alert notifications for a user."""
try:
Expand Down
46 changes: 46 additions & 0 deletions application/single_app/functions_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@
USER_SETTINGS_REQUEST_CACHE_ATTR = "simplechat_user_settings_request_cache"
FONT_SIZE_PREFERENCES = ("xs", "s", "m", "l", "xl")
DEFAULT_FONT_SIZE_PREFERENCE = "m"
CHAT_COMPLETION_AUDIO_SOUND_IDS = (
"aurora",
"bell",
"bloom",
"chime",
"crystal",
"glimmer",
"marimba",
"pulse",
"spark",
"summit",
)
DEFAULT_CHAT_COMPLETION_AUDIO_SOUND = CHAT_COMPLETION_AUDIO_SOUND_IDS[0]
DEFAULT_CHAT_COMPLETION_AUDIO_VOLUME = 5
USER_UI_SETTINGS_KEYS = (
"profileImage",
"navLayout",
Expand All @@ -43,6 +57,10 @@
"sidebarMenuState",
LATEST_FEATURES_HIDDEN_VERSION_SETTING,
"fontSizePreference",
"chatCompletionAudioEnabled",
"chatCompletionAudioMuted",
"chatCompletionAudioSound",
"chatCompletionAudioVolume",
)
ADMIN_SETTINGS_SECRET_REDACTED_VALUE = "***REDACTED***"
ADMIN_SETTINGS_FORM_SECRET_FIELDS = (
Expand Down Expand Up @@ -141,6 +159,31 @@
return DEFAULT_FONT_SIZE_PREFERENCE


def normalize_chat_completion_audio_preferences(settings):
"""Return validated completion-audio preferences with opt-in defaults."""

Check warning on line 163 in application/single_app/functions_settings.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
source = settings if isinstance(settings, dict) else {}
selected_sound = str(
source.get("chatCompletionAudioSound") or DEFAULT_CHAT_COMPLETION_AUDIO_SOUND
).strip().lower()
if selected_sound not in CHAT_COMPLETION_AUDIO_SOUND_IDS:
selected_sound = DEFAULT_CHAT_COMPLETION_AUDIO_SOUND

try:

Check warning on line 171 in application/single_app/functions_settings.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
volume = int(source.get(
"chatCompletionAudioVolume",
DEFAULT_CHAT_COMPLETION_AUDIO_VOLUME,
))
except (TypeError, ValueError):
volume = DEFAULT_CHAT_COMPLETION_AUDIO_VOLUME

return {
"chatCompletionAudioEnabled": source.get("chatCompletionAudioEnabled") is True,
"chatCompletionAudioMuted": source.get("chatCompletionAudioMuted") is True,
"chatCompletionAudioSound": selected_sound,
"chatCompletionAudioVolume": min(10, max(1, volume)),
}


def _get_user_settings_request_cache():
if not has_request_context():
return None
Expand Down Expand Up @@ -183,6 +226,7 @@
ui_settings["fontSizePreference"] = normalize_font_size_preference(
settings.get("fontSizePreference")
)
ui_settings.update(normalize_chat_completion_audio_preferences(settings))
return ui_settings


Expand Down Expand Up @@ -1020,6 +1064,8 @@
# Multimedia
'enable_video_file_support': False,
'enable_audio_file_support': False,
'enable_chat_completion_audio_cues': False,
'chat_completion_audio_cues_updated_at': None,

# Metadata Extraction
'enable_extract_meta_data': False,
Expand Down
90 changes: 89 additions & 1 deletion application/single_app/route_backend_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from config import *
from functions_authentication import *
from functions_appinsights import log_event

Check warning on line 5 in application/single_app/route_backend_notifications.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
from functions_conversation_cache import bump_conversation_cache_version
from functions_settings import *
from functions_notifications import *
Expand Down Expand Up @@ -71,10 +72,19 @@
try:
user_id = get_current_user_id()
count = get_unread_notification_count(user_id)
app_settings = get_settings()
completion_audio_enabled = bool(
app_settings.get("enable_chat_completion_audio_cues", False)
)
completion_audio_updated_at = app_settings.get(
"chat_completion_audio_cues_updated_at"
)

return jsonify({
'success': True,
'count': count
'count': count,
'chat_completion_audio_enabled': completion_audio_enabled,
'chat_completion_audio_updated_at': completion_audio_updated_at,
})

except Exception as e:
Expand All @@ -84,6 +94,84 @@
'count': 0
}), 500

@bp.route("/api/notifications/chat-completions", methods=["GET"])
@swagger_route(security=get_auth_security())
@login_required
@user_required
def api_get_chat_completion_notifications():
"""Get recent personal chat completion event identities for audio cues."""
user_id = None
try:
user_id = get_current_user_id()
app_settings = get_settings()
completion_audio_enabled = bool(
app_settings.get("enable_chat_completion_audio_cues", False)
)
completion_audio_updated_at = app_settings.get(
"chat_completion_audio_cues_updated_at"
)
if not completion_audio_enabled:
return jsonify({
"success": True,
"enabled": False,
"updated_at": completion_audio_updated_at,
"notifications": [],
})
limit = request.args.get("limit", 50)
notifications = get_recent_chat_response_notifications(user_id, limit=limit)
return jsonify({
"success": True,
"enabled": True,
"updated_at": completion_audio_updated_at,
"notifications": notifications,
})
except Exception as e:
log_event(
"[Notifications] Chat completion event request failed.",
extra={
"user_id": user_id,
"error": str(e),
},
level=logging.ERROR,
exceptionTraceback=True,
)
return jsonify({
"success": False,
"notifications": [],
"error": "Failed to fetch chat completion events",
}), 500

@bp.route("/api/notifications/chat-completion-audio-status", methods=["GET"])
@swagger_route(security=get_auth_security())
@login_required
@user_required
def api_get_chat_completion_audio_status():
"""Return the current server-authoritative completion audio gate."""
try:
app_settings = get_settings()
enabled = bool(
app_settings.get("enable_chat_completion_audio_cues", False)
)
return jsonify({
"success": True,
"enabled": enabled,
"updated_at": app_settings.get(
"chat_completion_audio_cues_updated_at"
),
})
except Exception as e:
log_event(
"[Notifications] Completion audio status request failed.",
extra={"error": str(e)},
level=logging.ERROR,
exceptionTraceback=True,
)
return jsonify({
"success": False,
"enabled": False,
"error": "Failed to load completion audio status",
}), 500

@bp.route("/api/notifications/workflow-alerts", methods=["GET"])
@swagger_route(security=get_auth_security())
@login_required
Expand Down
33 changes: 33 additions & 0 deletions application/single_app/route_backend_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,9 @@ def user_settings():
'microphonePermissionPreference', 'microphonePermissionState',
# Text-to-speech settings
'ttsEnabled', 'ttsVoice', 'ttsSpeed', 'ttsAutoplay',
# AI response completion audio settings
'chatCompletionAudioEnabled', 'chatCompletionAudioMuted',
'chatCompletionAudioSound', 'chatCompletionAudioVolume',
# Tutorial visibility settings
'showTutorialButtons',
# Desktop conversation notification settings
Expand Down Expand Up @@ -562,6 +565,36 @@ def user_settings():
if not isinstance(settings_to_update["conversationContentsDrawerEnabled"], bool):
return jsonify({"error": "Invalid conversation contents drawer preference"}), 400

for boolean_key in (
"chatCompletionAudioEnabled",
"chatCompletionAudioMuted",
):
if (
boolean_key in settings_to_update
and not isinstance(settings_to_update[boolean_key], bool)
):
return jsonify({"error": f"Invalid {boolean_key} preference"}), 400

if "chatCompletionAudioSound" in settings_to_update:
selected_sound = str(
settings_to_update.get("chatCompletionAudioSound") or ""
).strip().lower()
if selected_sound not in CHAT_COMPLETION_AUDIO_SOUND_IDS:
return jsonify({"error": "Invalid completion audio sound"}), 400
settings_to_update["chatCompletionAudioSound"] = selected_sound

if "chatCompletionAudioVolume" in settings_to_update:
volume = settings_to_update.get("chatCompletionAudioVolume")
if isinstance(volume, bool):
return jsonify({"error": "Invalid completion audio volume"}), 400
try:
volume = int(volume)
except (TypeError, ValueError):
return jsonify({"error": "Invalid completion audio volume"}), 400
if volume < 1 or volume > 10:
return jsonify({"error": "Completion audio volume must be between 1 and 10"}), 400
settings_to_update["chatCompletionAudioVolume"] = volume

if "desktopNotificationsEnabled" in settings_to_update:
if not isinstance(settings_to_update["desktopNotificationsEnabled"], bool):
return jsonify({"error": "Invalid desktop notification preference"}), 400
Expand Down
10 changes: 10 additions & 0 deletions application/single_app/route_frontend_admin_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,14 @@ def parse_admin_int(raw_value, fallback_value, field_name="unknown", hard_defaul
# ... (fetch all other fields using form_data.get) ...
enable_video_file_support = form_data.get('enable_video_file_support') == 'on'
enable_audio_file_support = form_data.get('enable_audio_file_support') == 'on'
enable_chat_completion_audio_cues = form_data.get('enable_chat_completion_audio_cues') == 'on'
chat_completion_audio_cues_updated_at = settings.get(
'chat_completion_audio_cues_updated_at'
)
if enable_chat_completion_audio_cues != bool(
settings.get('enable_chat_completion_audio_cues', False)
):
chat_completion_audio_cues_updated_at = datetime.now(timezone.utc).isoformat()
enable_extract_meta_data = form_data.get('enable_extract_meta_data') == 'on'

# Vision settings
Expand Down Expand Up @@ -2689,6 +2697,8 @@ def is_valid_url(url):
'video_index_timeout': int(form_data.get('video_index_timeout', 600)),

# Audio file settings with Azure speech service
'enable_chat_completion_audio_cues': enable_chat_completion_audio_cues,
'chat_completion_audio_cues_updated_at': chat_completion_audio_cues_updated_at,
'speech_service_endpoint': form_data.get('speech_service_endpoint', '').strip(),
'speech_service_location': form_data.get('speech_service_location', '').strip(),
'speech_service_subscription_id': form_data.get('speech_service_subscription_id', '').strip(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
SimpleChat Completion Cue Assets

The ten WAV files in this directory were generated specifically for the
SimpleChat project from synthesized sine waves and contain no third-party
recordings or samples. They are distributed under the same license as the
SimpleChat repository.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
14 changes: 14 additions & 0 deletions application/single_app/static/js/chat/chat-streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ function markStreamingConversationReadIfActive(conversationId, contextLabel) {
});
}

function notifySuccessfulStreamingCompletion(finalData) {
if (!window.simpleChatCompletionAudio?.handleCompletion) {
return;
}

window.simpleChatCompletionAudio.handleCompletion({
conversationId: finalData?.conversation_id,
messageId: finalData?.message_id,
}, {
refreshAdminGate: true,
});
}

function buildDefaultCancelEndpoint(conversationId) {
const normalizedConversationId = String(conversationId || '').trim();
if (!normalizedConversationId) {
Expand Down Expand Up @@ -1244,6 +1257,7 @@ function handleStreamError(messageId, partialContent, errorMessage, errorDetails

function finalizeStreamingMessage(messageId, userMessageId, finalData, fallbackAgentInfo = null) {
finalData = applyFallbackAgentIcon(finalData, fallbackAgentInfo);
notifySuccessfulStreamingCompletion(finalData);
const messageElement = document.querySelector(`[data-message-id="${messageId}"]`);
if (!messageElement) return;

Expand Down
Loading
Loading