Skip to content
Merged
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.054"
VERSION = "0.261.061"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
4 changes: 4 additions & 0 deletions application/single_app/functions_appinsights.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,17 @@
"attempt",
"container",
"containername",
"diagnosticrunid",
"endpointfingerprint",
"error",
"errortype",
"failurereasons",
"failuresummary",
"indexname",
"httpattemptid",
"jobid",
"operation",
"providerrequestid",
"reason",
"resource",
"resourcename",
Expand Down
88 changes: 88 additions & 0 deletions application/single_app/functions_content_safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@

CONTENT_SAFETY_VIOLATION_MESSAGE_DEFAULT = 'Your message was blocked by Content Safety.'
CONTENT_SAFETY_VIOLATION_MESSAGE_MAX_LENGTH = 3000
CONTENT_SAFETY_CATEGORY_THRESHOLD_DEFAULT = 4
CONTENT_SAFETY_CATEGORY_THRESHOLD_LOW = 2
CONTENT_SAFETY_CATEGORY_THRESHOLD_MEDIUM = 4
CONTENT_SAFETY_CATEGORY_THRESHOLD_HIGH = 6
CONTENT_SAFETY_CATEGORY_KEYS = ('Violence', 'Hate', 'Sexual', 'SelfHarm')
CONTENT_SAFETY_CATEGORY_LABELS = {
'Violence': 'Violence',
'Hate': 'Hate',
'Sexual': 'Sexual',
'SelfHarm': 'Self-harm',
}
CONTENT_SAFETY_CATEGORY_KEY_ALIASES = {
'violence': 'Violence',
'hate': 'Hate',
'sexual': 'Sexual',
'selfharm': 'SelfHarm',
}


def normalize_content_safety_violation_message(value):
Expand All @@ -13,6 +30,77 @@ def normalize_content_safety_violation_message(value):
return candidate[:CONTENT_SAFETY_VIOLATION_MESSAGE_MAX_LENGTH]


def normalize_content_safety_category_key(value):
"""Return a supported Azure Content Safety category key."""
candidate = str(value or '').strip()
if candidate in CONTENT_SAFETY_CATEGORY_KEYS:
return candidate
normalized = (
candidate.lower()
.replace(' ', '')
.replace('-', '')
.replace('_', '')
)
if '.' in normalized:
normalized = normalized.rsplit('.', 1)[-1]
return CONTENT_SAFETY_CATEGORY_KEY_ALIASES.get(normalized)


def normalize_content_safety_category_threshold(value):
"""Return a trimmed Azure Content Safety threshold: low, medium, or high."""
try:
candidate = int(value)
except (TypeError, ValueError):
return CONTENT_SAFETY_CATEGORY_THRESHOLD_DEFAULT

if candidate <= CONTENT_SAFETY_CATEGORY_THRESHOLD_LOW:
return CONTENT_SAFETY_CATEGORY_THRESHOLD_LOW
if candidate <= CONTENT_SAFETY_CATEGORY_THRESHOLD_MEDIUM:
return CONTENT_SAFETY_CATEGORY_THRESHOLD_MEDIUM
return CONTENT_SAFETY_CATEGORY_THRESHOLD_HIGH


def normalize_content_safety_category_thresholds(value, legacy_threshold=None):
"""Return category-specific Content Safety block thresholds."""
legacy_default = normalize_content_safety_category_threshold(legacy_threshold)
raw_thresholds = value if isinstance(value, dict) else {}
return {
category_key: normalize_content_safety_category_threshold(
raw_thresholds.get(category_key, legacy_default)
)
for category_key in CONTENT_SAFETY_CATEGORY_KEYS
}


def get_content_safety_category_thresholds(settings):
"""Read category-specific severity thresholds from the settings document."""
if not isinstance(settings, dict):
return normalize_content_safety_category_thresholds({})
return normalize_content_safety_category_thresholds(
settings.get('content_safety_category_thresholds'),
legacy_threshold=settings.get('content_safety_block_threshold'),
)


def get_content_safety_category_threshold(settings, category):
"""Read the severity threshold for one Content Safety category."""
category_key = normalize_content_safety_category_key(category)
if not category_key:
return CONTENT_SAFETY_CATEGORY_THRESHOLD_DEFAULT
return get_content_safety_category_thresholds(settings).get(
category_key,
CONTENT_SAFETY_CATEGORY_THRESHOLD_DEFAULT,
)


def get_content_safety_category_label(category):
"""Return a user-facing Content Safety category label."""
category_key = normalize_content_safety_category_key(category)
if not category_key:
return str(category or '').strip() or 'Unknown'
return CONTENT_SAFETY_CATEGORY_LABELS[category_key]


def build_content_safety_violation_message(
settings,
block_reasons,
Expand Down
6 changes: 5 additions & 1 deletion application/single_app/functions_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,8 @@ def resolve_vision_model_endpoint(settings, vision_model):
return find_enabled_model_endpoint_for_model_name(endpoints, vision_model)


def _resolve_metadata_extraction_client(settings, identity_context=None):
def resolve_metadata_extraction_client(settings, identity_context=None):
"""Resolve the metadata extraction chat client and request model from configured settings."""
selection = _normalize_model_endpoint_selection(settings.get("metadata_extraction_model_selection"))

if (
Expand Down Expand Up @@ -361,6 +362,9 @@ def _resolve_metadata_extraction_client(settings, identity_context=None):
), gpt_model


_resolve_metadata_extraction_client = resolve_metadata_extraction_client


ARCHIVED_SCOPE_PREFIX = "__archived__::"
CURRENT_ALIAS_BLOB_PATH_MODE = "current_alias"
ARCHIVED_REVISION_BLOB_PATH_MODE = "archived_revision"
Expand Down
16 changes: 16 additions & 0 deletions application/single_app/functions_model_endpoint_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
)
from functions_model_endpoint_validation import validate_custom_model_endpoint_url
from functions_settings import resolve_model_endpoint_foundry_scope
from functions_tabular_model_diagnostics import build_tabular_model_http_options
from model_endpoint_clients import (
MODEL_ENDPOINT_PROTOCOL_ANTHROPIC,
MODEL_ENDPOINT_PROTOCOL_AZURE_OPENAI,
Expand Down Expand Up @@ -63,6 +64,21 @@


def _build_azure_chat_completion(default_headers=None, **kwargs):
diagnostic_options = build_tabular_model_http_options(kwargs.get('service_id'))
if diagnostic_options:
client = AsyncAzureOpenAI(
azure_endpoint=kwargs.get('endpoint'),
azure_deployment=kwargs.get('deployment_name'),
api_version=kwargs.get('api_version'),
api_key=kwargs.get('api_key'),
azure_ad_token=kwargs.get('ad_token'),
azure_ad_token_provider=kwargs.get('azure_ad_token_provider') or kwargs.get('ad_token_provider'),
default_headers=default_headers,
**diagnostic_options,
)
service_options = dict(kwargs)
service_options.pop('azure_ad_token_provider', None)
return AzureChatCompletion(async_client=client, **service_options)
if not default_headers:
return AzureChatCompletion(**kwargs)
try:
Expand Down
6 changes: 6 additions & 0 deletions application/single_app/functions_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from config import *
from functions_appinsights import log_event
from functions_content_safety import (
CONTENT_SAFETY_CATEGORY_KEYS,
CONTENT_SAFETY_CATEGORY_THRESHOLD_DEFAULT,
CONTENT_SAFETY_VIOLATION_MESSAGE_DEFAULT,
)
from functions_cosmos_throughput import get_default_cosmos_throughput_settings
Expand Down Expand Up @@ -1750,6 +1752,10 @@ def get_settings(use_cosmos=False, include_source=False):

# Safety (Content Safety) Settings
'enable_content_safety': False,
'content_safety_category_thresholds': {
category_key: CONTENT_SAFETY_CATEGORY_THRESHOLD_DEFAULT
for category_key in CONTENT_SAFETY_CATEGORY_KEYS
},
'content_safety_violation_message': CONTENT_SAFETY_VIOLATION_MESSAGE_DEFAULT,
'content_safety_include_trigger_information': True,
'require_member_of_safety_violation_admin': False,
Expand Down
139 changes: 139 additions & 0 deletions application/single_app/functions_tabular_model_diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# functions_tabular_model_diagnostics.py
"""Opt-in, metadata-only HTTP diagnostics for foreground tabular model calls."""

import asyncio
import hashlib
import logging
import math
import os
import re
import time
import uuid

from openai import DefaultAsyncHttpxClient

from functions_appinsights import log_event


DIAGNOSTICS_ENV = 'SIMPLECHAT_TABULAR_HTTP_DIAGNOSTICS'
TIMEOUT_ENV = 'SIMPLECHAT_TABULAR_HTTP_TIMEOUT_SECONDS'
RETRIES_ENV = 'SIMPLECHAT_TABULAR_HTTP_MAX_RETRIES'
BUDGET_ENV = 'SIMPLECHAT_TABULAR_HTTP_BUDGET_SECONDS'


def _diagnostic_number(name, default, minimum, maximum, integer=False):
try:
raw_value = os.environ.get(name, str(default))
value = int(raw_value) if integer else float(raw_value)
if math.isfinite(value) and minimum <= value <= maximum:
return value
except (ValueError, TypeError, OverflowError):
pass
return default


def _safe_provider_request_id(response):
for name in ('x-request-id', 'apim-request-id', 'x-ms-request-id'):
value = response.headers.get(name, '')
if re.fullmatch(r'[A-Za-z0-9_-]{1,128}', value):
return value
return ''


class TabularDiagnosticAsyncClient(DefaultAsyncHttpxClient):
"""Observe SDK HTTP attempts without inspecting request or response bodies."""

def __init__(self, *, budget_seconds, **kwargs):
super().__init__(**kwargs)
self.diagnostic_run_id = uuid.uuid4().hex
self.budget_seconds = budget_seconds
self._first_attempt_at = None
self._attempt_sequence = 0

def _emit(self, operation, properties, level=logging.INFO):
try:
log_event(
f'[TABULAR_MODEL_HTTP] {operation}',
extra={
'diagnostic_run_id': self.diagnostic_run_id,
'operation': operation,
'service': 'tabular-analysis',
**properties,
},
level=level,
)
except Exception:
pass

async def send(self, request, **kwargs):
started_at = time.monotonic()
if self._first_attempt_at is None:
self._first_attempt_at = started_at
self._attempt_sequence += 1
try:
retry_count = max(0, int(request.headers.get('x-stainless-retry-count', '0')))
except (ValueError, TypeError):
retry_count = 0
endpoint_identity = f'{request.url.host}:{request.url.port}{request.url.path}'
properties = {
'http_attempt_id': uuid.uuid4().hex,
'attempt_sequence': self._attempt_sequence,
'retry_count': retry_count,
'endpoint_fingerprint': hashlib.sha256(endpoint_identity.encode('utf-8')).hexdigest()[:16],
'budget_seconds': self.budget_seconds,
}
self._emit('attempt_started', properties)
try:
remaining = self.budget_seconds - (started_at - self._first_attempt_at)
if remaining <= 0:
raise TimeoutError('Tabular diagnostic HTTP budget exhausted')
async with asyncio.timeout(remaining):
response = await super().send(request, **kwargs)
except (Exception, asyncio.CancelledError) as exc:
cause = exc
seen = set()
while cause.__cause__ is not None and id(cause) not in seen:
seen.add(id(cause))
cause = cause.__cause__
self._emit('attempt_failed', {
**properties,
'duration_ms': round((time.monotonic() - started_at) * 1000, 3),
'error_type': type(exc).__name__,
'reason': type(cause).__name__,
'budget_exhausted': time.monotonic() - self._first_attempt_at >= self.budget_seconds,
}, level=logging.WARNING)
raise
self._emit('attempt_completed', {
**properties,
'duration_ms': round((time.monotonic() - started_at) * 1000, 3),
'status_code': response.status_code,
'success': response.is_success,
'provider_request_id': _safe_provider_request_id(response),
})
return response


def build_tabular_model_http_options(service_id):
"""Return SDK options only for explicitly enabled tabular-analysis clients."""
enabled = os.environ.get(DIAGNOSTICS_ENV, '').strip().lower() in {'1', 'true', 'yes', 'on'}
if service_id != 'tabular-analysis' or not enabled:
return {}
timeout = _diagnostic_number(TIMEOUT_ENV, 60, 1, 600)
retries = _diagnostic_number(RETRIES_ENV, 0, 0, 2, integer=True)
budget = _diagnostic_number(BUDGET_ENV, 180, 1, 1800)
return {
'http_client': TabularDiagnosticAsyncClient(budget_seconds=budget, timeout=timeout),
'timeout': timeout,
'max_retries': retries,
}


async def close_tabular_model_http_client(service):
"""Close only the explicitly owned diagnostic SDK client after a tabular run."""
sdk_client = getattr(service, 'client', None)
http_client = getattr(sdk_client, '_client', None)
if isinstance(http_client, TabularDiagnosticAsyncClient):
try:
await sdk_client.close()
except Exception as exc:
http_client._emit('cleanup_failed', {'error_type': type(exc).__name__}, level=logging.WARNING)
Loading