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
11 changes: 11 additions & 0 deletions application/single_app/admin_settings_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -8508,6 +8508,17 @@
"default": False,
},
],
"support-log-export-section": [
{
"type": "component",
"component": "support-log-export",
"label": "Support Log Export",
"help": (
"Collect retained diagnostics by user, conversation and time into a private, "

Check warning on line 8517 in application/single_app/admin_settings_fields.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
"redacted support ZIP. Whole personal-conversation snapshots need their owner's approval."

Check warning on line 8518 in application/single_app/admin_settings_fields.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

Check warning on line 8518 in application/single_app/admin_settings_fields.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.
),
},
],
"debug-logging-section": [
{
"key": "enable_debug_logging",
Expand Down
1 change: 1 addition & 0 deletions application/single_app/admin_settings_nav.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@
"sections": [
{"id": "application-insights-section", "label": "Application Insights", "icon": "bi-graph-up"},
{"id": "debug-logging-section", "label": "Debug Logging", "icon": "bi-bug"},
{"id": "support-log-export-section", "label": "Support Log Export", "icon": "bi-file-earmark-zip"},
{"id": "file-processing-logs-section", "label": "File Process Logging", "icon": "bi-file-earmark-text"},
{"id": "health-check-section", "label": "Health Check", "icon": "bi-heart-pulse"},
{"id": "swagger-section", "label": "API Documentation", "icon": "bi-file-earmark-code"},
Expand Down
21 changes: 20 additions & 1 deletion application/single_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
import threading
import time
from datetime import datetime
from flask import Blueprint, g, make_response
from flask import Blueprint, g, has_request_context, make_response
from urllib.parse import urlencode, urlparse

from route_frontend_authentication import *
Expand All @@ -69,6 +69,10 @@

from route_backend_chats import *
from route_backend_analysis_results import register_route_backend_analysis_results
from route_backend_support_exports import (
register_route_backend_support_exports,
register_route_backend_support_export_consents,
)
from route_backend_v2 import (
register_route_backend_v2,
register_route_backend_v2_admin,
Expand Down Expand Up @@ -391,6 +395,19 @@
initialize_application()


def _support_request_diagnostics():
if not has_request_context():
return {}
authorized = getattr(g, 'authorized_chat_context', None)

Check warning on line 401 in application/single_app/app.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
return {
'user_id': get_current_user_id_or_none(),
'conversation_id': authorized.get('conversation_id') if isinstance(authorized, dict) else None,

Check warning on line 404 in application/single_app/app.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
}


configure_diagnostic_context_provider(_support_request_diagnostics)


def get_idle_timeout_settings(settings=None):
"""
Resolve and normalize idle timeout settings used for warning and logout enforcement.
Expand Down Expand Up @@ -1581,6 +1598,8 @@

# ------------------- API Control Center Routes ---------
register_route_blueprint('backend_control_center', register_route_backend_control_center, login_required_blueprint)
register_route_blueprint('backend_support_exports', register_route_backend_support_exports, admin_required_blueprint)

Check warning on line 1601 in application/single_app/app.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.
register_route_blueprint('backend_support_export_consents', register_route_backend_support_export_consents, user_required_blueprint)

Check warning on line 1602 in application/single_app/app.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.

# ------------------- API Notifications Routes ----------
register_route_blueprint('backend_notifications', register_route_backend_notifications, user_required_blueprint)
Expand Down
16 changes: 16 additions & 0 deletions application/single_app/background_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
check_due_key_vault_secret_reminders_once,
)
from functions_tabular_generated_exports import check_due_tabular_generated_output_runs_once
from functions_support_exports import check_due_support_exports_once
from functions_personal_workflows import (
compute_next_run_at,
get_due_personal_workflows,
Expand Down Expand Up @@ -973,6 +974,20 @@
time.sleep(60)


def run_support_export_scheduler_loop(app=None):
"""Run durable, fenced exports in web workers or the dedicated scheduler."""
while True:
try:

Check warning on line 980 in application/single_app/background_tasks.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.
with app.app_context() if app is not None else nullcontext():
check_due_support_exports_once()
except Exception as exc:

Check warning on line 983 in application/single_app/background_tasks.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 984 in application/single_app/background_tasks.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.
"[SUPPORT_EXPORT] Scheduler slice failed.",
extra={"error_type": type(exc).__name__}, level=logging.ERROR,
)
time.sleep(15)


def run_document_search_metadata_sync_loop():
"""Retry search chunk metadata syncs that failed or never ran in their original process."""
while True:
Expand Down Expand Up @@ -1108,6 +1123,7 @@
('Orchestration scheduler background task started.', lambda: run_orchestration_scheduler_loop(app=app)),
('Workflow chat delivery background task started.', lambda: run_workflow_chat_delivery_loop(app=app)),
('Data Management scheduler background task started.', lambda: run_data_management_scheduler_loop(app=app)),
('Support export scheduler background task started.', lambda: run_support_export_scheduler_loop(app=app)),
('Document search metadata sync background task started.', run_document_search_metadata_sync_loop),
('Content screening scheduler background task started.', lambda: run_content_screening_scheduler_loop(app=app)),
('App maintenance background task started.', run_app_maintenance_loop),
Expand Down
9 changes: 8 additions & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
EXECUTOR_MAX_WORKERS = 30
CHAT_STREAM_EXECUTOR_MAX_WORKERS = max(8, int(os.getenv('SIMPLECHAT_CHAT_STREAM_WORKERS', '32') or '32'))
SESSION_TYPE = 'filesystem'
VERSION = "0.261.427"
VERSION = "0.261.428"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down Expand Up @@ -699,6 +699,13 @@ def _create_container_if_not_exists_with_conflict_recovery(*args, **kwargs):
partition_key=PartitionKey(path="/user_id")
)

cosmos_support_exports_container_name = "support_exports"
cosmos_support_exports_container = cosmos_database.create_container_if_not_exists(
id=cosmos_support_exports_container_name,
partition_key=PartitionKey(path="/partition_key"),
default_ttl=-1,
)

cosmos_data_management_jobs_container_name = "data_management_jobs"
DATA_MANAGEMENT_HISTORY_INDEXING_POLICY = {
"indexingMode": "consistent",
Expand Down
28 changes: 24 additions & 4 deletions application/single_app/content_screening/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1060,13 +1060,18 @@ def _checkpoint_record(window, check, fingerprint, model_fingerprint, findings,

def evaluate_model_units(
units: list[ContentUnit], check: dict, *, settings=None, client=None, on_progress=None,
checkpoint_store=None,
checkpoint_store=None, token_ceiling=None, input_ceiling=None, binding_validator=None,
) -> DetectorResult:
"""Inspect every required window or return a safe error/incomplete result.

Shared policy limits bound source size, windows, wall time, and findings.
Output tokens, total tokens, response size, and per-request time have internal
hard ceilings, not additional editable policy keys.
Trusted callers may lower ``token_ceiling`` and ``input_ceiling`` for an
operation. Conservative complete-window reservations are checked before
inference; defaults preserve the document scanner's existing budgets.
A trusted ``binding_validator`` may additionally require the freshly resolved
binding to match the host's disclosure before constructing an inference client.
Missing/placeholder provider usage is charged a conservative request/output reservation,
not reported as measured token usage. Progress never includes evidence or IDs.
``usage.coverage`` binds the canonical source/rule and lists only fully
Expand Down Expand Up @@ -1117,6 +1122,13 @@ def evaluate_model_units(
if checkpoint_store is not None:
result.usage["checkpoint_windows"] = 0
try:
total_token_limit = _MODEL_MAX_TOTAL_TOKENS if token_ceiling is None else token_ceiling
if (
type(total_token_limit) is not int or not 1 <= total_token_limit <= _MODEL_MAX_TOTAL_TOKENS
or (input_ceiling is not None and (type(input_ceiling) is not int or input_ceiling < 1))
or (binding_validator is not None and not callable(binding_validator))
):
raise ScreeningValidationError()
if isinstance(units, (list, tuple)):
result.required_units = len(units)
units = normalize_units(units)
Expand Down Expand Up @@ -1155,10 +1167,18 @@ def evaluate_model_units(
)
)
binding = _resolve_binding(settings, check["model_selection"])
if binding_validator is not None and binding_validator(binding) is not True:
raise _ModelFailure("model_configuration_unavailable")
client, deployment, protocol, behavior = _inference_configuration(
binding, settings, client, deadline, result, on_progress,
initialize_client=checkpoint_store is None,
)
if token_ceiling is not None or input_ceiling is not None:
reservations = [_parameters(window, check, binding, deployment, protocol, behavior)[1] for window in windows]
if sum(reservations) > total_token_limit:
raise _ModelFailure("model_token_limit", status="incomplete")
if input_ceiling is not None and any(reservation > input_ceiling for reservation in reservations):
raise _ModelFailure("model_input_limit", status="incomplete")
checkpoint_context, model_fingerprint = (
_checkpoint_context(
checkpoint_store, binding, check, check_metadata, planner.source_fingerprint, protocol,
Expand All @@ -1184,13 +1204,13 @@ def evaluate_model_units(
)
if cached is not None:
findings, window_usage = cached
if result.usage["budgeted_tokens"] + window_usage["budgeted_tokens"] > _MODEL_MAX_TOTAL_TOKENS:
if result.usage["budgeted_tokens"] + window_usage["budgeted_tokens"] > total_token_limit:
raise _ModelFailure("model_token_limit", status="incomplete")
for name in _MODEL_USAGE_FIELDS:
result.usage[name] += window_usage[name]
result.usage["checkpoint_windows"] += 1
else:
if result.usage["budgeted_tokens"] + reservation > _MODEL_MAX_TOTAL_TOKENS:
if result.usage["budgeted_tokens"] + reservation > total_token_limit:
raise _ModelFailure("model_token_limit", status="incomplete")
_emit_progress(result, on_progress)
if client is None:
Expand All @@ -1209,7 +1229,7 @@ def evaluate_model_units(
reported = _record_usage(response, result)
if reported is not None:
result.usage["budgeted_tokens"] += reported - reservation
if result.usage["budgeted_tokens"] > _MODEL_MAX_TOTAL_TOKENS:
if result.usage["budgeted_tokens"] > total_token_limit:
raise _ModelFailure("model_token_limit", status="incomplete")
content = _response_text(response, _MODEL_MAX_RESPONSE_CHARACTERS)
result.usage["output_characters"] += len(content)
Expand Down
47 changes: 45 additions & 2 deletions application/single_app/functions_appinsights.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import re
import sys
import threading
from contextlib import contextmanager
from contextvars import ContextVar
from datetime import date, datetime
from typing import Any, Dict, Optional, Tuple

Expand All @@ -20,6 +22,8 @@
_appinsights_logger = None
_azure_monitor_configured = False
_logging_settings_load_state = threading.local()
_diagnostic_context = ContextVar("simplechat_diagnostic_context", default=None)
_diagnostic_context_provider = None
CONSOLE_ERROR_HANDLER_ATTRIBUTE = "_simplechat_console_error_handler"
CONSOLE_ERROR_LOG_FORMAT = "[%(asctime)s] %(levelname)s in %(name)s: %(message)s"
REDACTED_LOG_VALUE = "***REDACTED***"
Expand Down Expand Up @@ -133,7 +137,7 @@
"taskname",
})
LOGGER_WORKFLOW_HASH_KEYS = frozenset({
"conversationidhash", "turnidhash", "runidhash", "stepidhash", "producerstepidhash",
"useridhash", "conversationidhash", "turnidhash", "runidhash", "stepidhash", "producerstepidhash",
"documentidhash", "plannerbindinghash", "documentsethash",
"consumerrunidhash",
"nativecursorhash", "nextnativecursorhash",
Expand All @@ -147,6 +151,41 @@
})


def configure_diagnostic_context_provider(provider):
"""The web owner supplies request context without importing Flask/bootstrap here."""
global _diagnostic_context_provider
if not callable(provider):
raise TypeError("A diagnostic context provider must be callable.")
_diagnostic_context_provider = provider


@contextmanager
def diagnostic_log_context(*, user_id=None, conversation_id=None):
"""Explicit, isolated correlation for non-request and background work."""
context = dict(_diagnostic_context.get() or {})
context.update({key: value for key, value in (
("user_id", user_id), ("conversation_id", conversation_id),
) if value})
token = _diagnostic_context.set(context)
try:
yield
finally:
_diagnostic_context.reset(token)


def _diagnostic_hashes(extra):
context = dict(_diagnostic_context.get() or {})
if _diagnostic_context_provider is not None:
context.update(_diagnostic_context_provider())
fields = {}
extra = extra if isinstance(extra, dict) else {}
for key in ("user_id", "conversation_id"):
value = context.get(key) or (extra or {}).get(key)
if isinstance(value, str) and 0 < len(value) <= 128:
fields[f"sc_{key}_hash"] = hashlib.sha256(value.encode("utf-8", errors="replace")).hexdigest()
return fields


def workflow_log_context(*, conversation_id=None, turn_id=None, run_id=None, step_id=None):
"""Correlate workflow requests without recording their raw identifiers."""
return {
Expand Down Expand Up @@ -361,6 +400,7 @@ def _build_logger_extra(
else:
logger_extra[normalized_key] = _logger_safe_scalar(value)

logger_extra.update(_diagnostic_hashes(extra))
return logger_extra


Expand Down Expand Up @@ -582,7 +622,8 @@ def log_event(
includeStack (bool, optional): If True, includes the current stack trace in the log.
stacklevel (int, optional): How many levels up the stack to report as the source.
exceptionTraceback (Any, optional): If set to True, includes exception traceback.
debug_only (bool, optional): If True, emit only debug-gated console output.
debug_only (bool, optional): If True, emit debug-gated console output and a
tagged Application Insights debug trace when the exporter is configured.
category (str, optional): Category label used for debug-only console output.
flush (bool, optional): Flush console output immediately for debug-only output.
message_args (tuple, optional): Optional printf-style formatting arguments.
Expand All @@ -594,6 +635,8 @@ def log_event(

if debug_only:
_emit_debug_message(cache, formatted_message, category, flush, safe_extra)
if cache.get('enable_debug_logging', False):
_emit_appinsights_debug_trace(formatted_message, category, safe_extra)
return

try:
Expand Down
Loading
Loading