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
342 changes: 270 additions & 72 deletions application/single_app/admin_settings_fields.py

Large diffs are not rendered by default.

14 changes: 9 additions & 5 deletions application/single_app/admin_settings_nav.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,15 @@
"icon": "bi-file-earmark-text",
"sections": [
{"id": "document-intelligence-section", "label": "Document Intelligence", "icon": "bi-file-earmark-text"},
# These two cards have always existed in the extraction pane but
# were absent from this list, so neither interface could
# navigate to them. Their ids are the ones already in the
# markup, so the server-rendered sidebar resolves them as-is.
{"id": "content-understanding-section", "label": "Content Understanding", "icon": "bi-stars"},
# Enhanced extraction and the Content Understanding connection
# that backs it are one section, led by the switch. Content
# Understanding used to be a section of its own, which let it be
# configured while Enhanced was off and it could never run.
{"id": "enhanced-extraction-section", "label": "Enhanced Extraction", "icon": "bi-stars"},
# This card has always existed in the extraction pane but was
# absent from this list, so neither interface could navigate to
# it. Its id is the one already in the markup, so the
# server-rendered sidebar resolves it as-is.
{"id": "office-embedded-image-section", "label": "Images Inside Office Files", "icon": "bi-images"},
{"id": "chunk-size-section", "label": "Chunk Sizes", "icon": "bi-collection"},
# The upload ceiling belongs with the extraction pipeline it
Expand Down
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.264"
VERSION = "0.261.265"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
10 changes: 9 additions & 1 deletion application/single_app/route_backend_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
is_admin_settings_redacted_secret,
is_chat_file_upload_enabled_for_user,
is_chat_workflow_results_enabled_for_user,
is_content_understanding_supported_environment,
is_user_workflows_enabled_for_user,
is_workflow_assistant_enabled_for_user,
merge_model_endpoint_payload,
Expand Down Expand Up @@ -1409,7 +1410,14 @@ def v2_admin_get_settings():
# rather than a stored setting. Inbound MCP is gated by an
# App Service application setting, so its value cannot be
# read out of the settings document the SPA already holds.
"runtime_flags": {"mcp_ui_enabled": is_mcp_ui_enabled()},
# Content Understanding is not offered in every Azure cloud,
# which only the server's AZURE_ENVIRONMENT can say.
"runtime_flags": {
"mcp_ui_enabled": is_mcp_ui_enabled(),
"content_understanding_supported": (
is_content_understanding_supported_environment()
),
},
"suppressed_capabilities": get_suppressed_capability_keys(),
"version": VERSION,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ <h5><i class="bi bi-file-earmark-text me-2"></i>Document Intelligence</h5>
Configure Azure Document Intelligence settings. Document Intelligence always powers
<strong>Standard</strong> extraction and is required for workspaces and chat file uploads.
</p>
<div class="form-group form-check form-switch mb-3 d-flex align-items-center">
<!-- Enhanced Extraction: the shared navigation's enhanced-extraction-section lands on this switch -->
<div class="form-group form-check form-switch mb-3 d-flex align-items-center" id="enhanced-extraction-section">
<input
type="checkbox"
class="form-check-input me-2"
Expand Down
126 changes: 126 additions & 0 deletions application/v2_ui/src/components/admin/EnhancedExtractionEngine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// EnhancedExtractionEngine.tsx
// Says which engine backs Enhanced extraction, judged from the settings on screen.
//
// Enhanced extraction runs on Azure AI Content Understanding where the service is offered
// and connected, and on Document Intelligence Layout otherwise. Only Content Understanding
// describes figures and charts, yet nothing on the card used to say which was in force, so
// the Enhanced switch and the Content Understanding connection read as unrelated settings.
// This names the engine, and when it is the fallback, why, and what would change it.

import { clsx } from 'clsx';
import { AlertCircle, CheckCircle2, Info } from 'lucide-react';
import type { EnhancedExtractionEngineReading } from '../../lib/enhancedExtraction';

interface EnhancedExtractionEngineProps {
label: string;
help?: string;
reading: EnhancedExtractionEngineReading;
/** The reading rests on edits that are not saved yet, so it is not in force. */
pending?: boolean;
}

interface EngineCopy {
title: string;
detail: string;
}

function describe(reading: EnhancedExtractionEngineReading): EngineCopy {
if (reading.engine === 'content_understanding') {
return {
title: 'Azure AI Content Understanding',
detail:
'Documents extracted as Enhanced get tables, page structure, checkbox states ' +
'and descriptions of figures and charts. If the service cannot be reached, a ' +
'document falls back to Document Intelligence Layout and the reason is ' +
'recorded on it.',
};
}

switch (reading.reason) {
case 'unsupported_cloud':
return {
title: 'Document Intelligence Layout',
detail:
'Azure AI Content Understanding is not offered in this Azure cloud. Layout ' +
'captures tables, page structure, forms and checkbox states, but does not ' +
'describe figures or charts. There is nothing more to configure.',
};
case 'missing_key':
return {
title: 'Document Intelligence Layout',
detail:
'Content Understanding has an endpoint but no key. Add the key, or set ' +
'Authentication Type to Managed Identity, to use it. Until then figures ' +
'and charts are not described.',
};
default:
return {
title: 'Document Intelligence Layout',
detail:
'Layout captures tables, page structure, forms and checkbox states, but ' +
'does not describe figures or charts. Add a Foundry endpoint under ' +
'Content Understanding connection to use Azure AI Content Understanding ' +
'instead.',
};
}
}

export function EnhancedExtractionEngine({
label,
help,
reading,
pending,
}: EnhancedExtractionEngineProps) {
const tone =
reading.engine === 'content_understanding'
? 'ok'
: reading.reason === 'unsupported_cloud'
? 'neutral'
: 'warn';
const Icon = tone === 'ok' ? CheckCircle2 : tone === 'warn' ? AlertCircle : Info;
const { title, detail } = describe(reading);

return (
<div
className="admin-field py-3"
data-field-width="wide"
data-testid="enhanced-extraction-engine"
data-engine={reading.engine}
>
<div className="admin-field-heading text-sm font-semibold text-text-1">{label}</div>
{help ? (
<p className="admin-field-help text-[0.8125rem] leading-relaxed text-text-3">{help}</p>
) : null}
<div className="admin-field-control min-w-0">
<div
role="status"

Check warning on line 96 in application/v2_ui/src/components/admin/EnhancedExtractionEngine.tsx

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.
aria-live="polite"
className={clsx(
'flex items-start gap-2.5 rounded-lg border p-3 text-xs leading-relaxed text-text-2',
tone === 'ok' && 'border-ok/40 bg-ok/5',
tone === 'warn' && 'border-warn/40 bg-warn-soft',
tone === 'neutral' && 'border-edge bg-surface-1',
)}
>
<Icon
size={15}
aria-hidden="true"

Check warning on line 107 in application/v2_ui/src/components/admin/EnhancedExtractionEngine.tsx

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.
className={clsx(
'mt-0.5 shrink-0',
tone === 'ok' && 'text-ok',
tone === 'warn' && 'text-warn',
tone === 'neutral' && 'text-text-3',
)}
/>
<div className="min-w-0">
<p className="font-medium text-text-1">{title}</p>
<p className="mt-1">{detail}</p>
{pending ? (
<p className="mt-1.5 text-text-3">Takes effect when you save.</p>
) : null}
</div>
</div>
</div>
</div>
);
}
21 changes: 18 additions & 3 deletions application/v2_ui/src/components/admin/SettingsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ export interface SettingsSectionProps {
forceExpanded?: boolean;
/** Opt-in presentation overrides; never changes the schema's behavior. */
appearance?: SettingsSectionAppearance;
/**
* Server-resolved runtime flags, such as whether Content Understanding is offered in
* this cloud. A field gated on one has to be judged the way the page judged it;
* without the flags its condition reads as unmet and the field is dropped from the
* card even though the page decided to show it.
*/
runtimeFlags?: Record<string, boolean>;
children?: ReactNode;
}

Expand Down Expand Up @@ -296,6 +303,7 @@ export function SettingsSection({
renderCapability,
forceExpanded,
appearance,
runtimeFlags,
children,
}: SettingsSectionProps) {
const capability = useMemo(() => findCapabilityField(fields), [fields]);
Expand Down Expand Up @@ -328,8 +336,10 @@ export function SettingsSection({
);

const groups = useMemo(
() => groupFields(bodyFields.filter((field) => isFieldVisible(field, settings, draft, fieldsByKey))),
[bodyFields, settings, draft, fieldsByKey],
() => groupFields(
bodyFields.filter((field) => isFieldVisible(field, settings, draft, fieldsByKey, runtimeFlags)),
),
[bodyFields, settings, draft, fieldsByKey, runtimeFlags],
);

const presentation = presentSectionStatus(status);
Expand Down Expand Up @@ -458,7 +468,12 @@ export function SettingsSection({
<FieldGroup
key={group.id || '__ungrouped'}
group={group}
startOpen={shouldGroupStartOpen(group, status, capabilityOn)}
startOpen={shouldGroupStartOpen(
group,
status,
capabilityOn,
(key) => readSectionValue(settings, draft, key, fieldsByKey),
)}
forceExpanded={forceExpanded}
renderFields={renderFields}
/>
Expand Down
73 changes: 73 additions & 0 deletions application/v2_ui/src/lib/adminFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ export interface AdminFieldGroup {
label?: string;
variant?: 'connection' | 'behavior' | 'limits' | 'access' | 'advanced';
help?: string;
/**
* A key in this group whose blank value opens the group while the section's
* capability is on.
*
* For an optional connection an administrator most likely wants next -- Content
* Understanding under Enhanced extraction -- which cannot be marked `required`
* because the capability works without it.
*/
open_until_set?: string;
}

/** Normalise either declared group shape into the object form. */
Expand Down Expand Up @@ -140,6 +149,20 @@ export interface AdminFieldAcknowledgement {
message: string;
}

/**
* Companion values a switch sets when it is turned on.
*
* Mirrors `on_enable` in `admin_settings_fields.py`. Turning Enhanced extraction on
* while the extraction mode still reads Standard would change nothing, so the mode
* moves to Auto. The page applies this to the draft as the switch flips, so the new
* value is on screen before saving, and only while `when` holds against the current
* values. The server applies the same rule to a save that omits the companion.
*/
export interface AdminFieldEnableEffect {
set: Record<string, string | number | boolean>;
when?: AdminFieldDependency;
}

export interface AdminField {
key?: string;
type: AdminFieldType;
Expand Down Expand Up @@ -262,6 +285,8 @@ export interface AdminField {
*/
scale?: number;
requires_acknowledgement?: AdminFieldAcknowledgement;
/** Switch fields only: companion values to set when it is turned on. */
on_enable?: AdminFieldEnableEffect;
}

/** Section id -> ordered fields. Section ids come from `admin_settings_nav.py`. */
Expand Down Expand Up @@ -482,6 +507,8 @@ export interface RenderedFieldGroup {
* always-on built-in actions out of the way.
*/
collapsed?: boolean;
/** A key whose blank value opens the group while the capability is on. */
openUntilSet?: string;
fields: AdminField[];
}

Expand Down Expand Up @@ -509,10 +536,15 @@ export function groupFields(fields: AdminField[]): RenderedFieldGroup[] {
variant: declared?.variant,
help: declared?.help,
collapsed: Boolean(field.collapsed),
openUntilSet: declared?.open_until_set,
fields: [],
};
byId.set(id, group);
groups.push(group);
} else if (!group.openUntilSet && declared?.open_until_set) {
// Declared once, usually on the group's first field, which may be the one
// a dependency is hiding.
group.openUntilSet = declared.open_until_set;
}
group.fields.push(field);
}
Expand Down Expand Up @@ -794,6 +826,47 @@ export function isRequirementSatisfied(
return asBoolean(current);
}

/**
* Apply a switch's `on_enable` companions to a draft, returning the new draft.
*
* Only a transition counts, as on the server: a switch that is already on when saved
* brings nothing with it. Turning it on sets each companion that the administrator has not
* already edited, while `when` holds against the values on screen. Turning it back off
* before saving takes back any companion still holding the value it set, so the draft only
* carries what the administrator chose. A key that has never been saved reads as its
* declared default, which is what the server would have merged in.
*/
export function applyEnableEffect(
draft: Json,
settings: Json,
field: AdminField,
next: boolean,
fieldsByKey?: Map<string, AdminField>,
): Json {
const effect = field.on_enable;
if (!field.key || !effect || asBoolean(settings[field.key])) {
return draft;
}

const read = (key: string): unknown => {
const value = readSettingValue(key, settings, draft, fieldsByKey);
return value === undefined ? fieldsByKey?.get(key)?.default : value;
};

const updated: Json = { ...draft };
for (const [companion, value] of Object.entries(effect.set ?? {})) {
const edited = Object.prototype.hasOwnProperty.call(draft, companion);
if (next) {
if (!edited && evaluateDependency(effect.when, read)) {
updated[companion] = value;
}
} else if (edited && draft[companion] === value) {
delete updated[companion];
}
}
return updated;
}

/**
* Whether a navigation section's `condition` holds.
*
Expand Down
9 changes: 9 additions & 0 deletions application/v2_ui/src/lib/adminSections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,17 @@ export function deriveFieldHierarchy(fields: AdminField[]): FieldHierarchy {
* the capability is on and something required is still blank; everything else stays shut.
* Turning a capability on therefore reveals the next step rather than forty controls, and
* a section that is already working stays a summary.
*
* An optional connection cannot be `required` -- Enhanced extraction works without
* Content Understanding -- so a group may instead name the key it waits on with
* `open_until_set`. It opens while that key is blank and the capability is on, which is
* when an administrator has just turned the capability on and is choosing an engine.
*/
export function shouldGroupStartOpen(
group: RenderedFieldGroup,
status: SectionStatus,
capabilityOn: boolean,
read?: (key: string) => unknown,
): boolean {
if (!group.id) {
// Ungrouped fields are the section's own preamble; collapsing them would hide
Expand All @@ -333,5 +339,8 @@ export function shouldGroupStartOpen(
if (!capabilityOn) {
return false;
}
if (group.openUntilSet && read && !hasValue(read(group.openUntilSet))) {
return true;
}
return group.variant === 'connection' && status === 'incomplete';
}
Loading
Loading