Skip to content

Commit b47cec4

Browse files
Merge pull request #623 from microsoft/dev
chore: Dev to Main Merge
2 parents 4113db3 + 1fee0b1 commit b47cec4

24 files changed

Lines changed: 464 additions & 83 deletions

File tree

src/ContentProcessor/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ colorama==0.4.6
1515
coverage==7.13.5
1616
cryptography==46.0.7
1717
dnspython==2.8.0
18-
idna==3.11
18+
idna==3.15
1919
iniconfig==2.3.0
2020
isodate==0.7.2
2121
mongomock==4.3.0

src/ContentProcessor/src/libs/pipeline/handlers/save_handler.py

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -112,20 +112,14 @@ def find_process_result(step_name: str):
112112
)
113113
)
114114

115-
total_evaluated_fields_count = evaluated_result.confidence.get(
116-
"total_evaluated_fields_count", 0
117-
)
118-
schema_score = (
119-
0
120-
if total_evaluated_fields_count == 0
121-
else round(
122-
(
123-
len(evaluated_result.comparison_result.items)
124-
- evaluated_result.confidence["zero_confidence_fields_count"]
125-
)
126-
/ len(evaluated_result.comparison_result.items),
127-
3,
128-
)
115+
# Compute the aggregate scores. Successful (Completed) processing
116+
# always yields numeric scores: when probabilistic confidence is
117+
# available (logprobs from non-reasoning models / Content Understanding
118+
# signal) we use it; otherwise we fall back to a structural
119+
# completeness score (fraction of expected fields actually filled).
120+
# Failed runs and genuinely empty extractions remain at ``0.0``.
121+
entity_score, schema_score, min_extracted_entity_score = (
122+
self._derive_aggregate_scores(evaluated_result)
129123
)
130124

131125
processed_result = ContentProcess(
@@ -143,11 +137,9 @@ def find_process_result(step_name: str):
143137
self._current_message_context.data_pipeline.pipeline_status.creation_time,
144138
"%Y-%m-%dT%H:%M:%S.%fZ",
145139
),
146-
entity_score=evaluated_result.confidence["overall_confidence"],
140+
entity_score=entity_score,
147141
schema_score=schema_score,
148-
min_extracted_entity_score=evaluated_result.confidence[
149-
"min_extracted_field_confidence"
150-
],
142+
min_extracted_entity_score=min_extracted_entity_score,
151143
prompt_tokens=evaluated_result.prompt_tokens,
152144
completion_tokens=evaluated_result.completion_tokens,
153145
target_schema=Schema.get_schema(
@@ -241,3 +233,85 @@ def _summarize_processed_time(self, step_results: list[StepResult]) -> str:
241233
# Format the total elapsed time as a string
242234
formatted_elapsed_time = f"{total_hours:02}:{total_minutes:02}:{total_seconds:02}.{total_milliseconds:03}"
243235
return formatted_elapsed_time
236+
237+
@staticmethod
238+
def _is_filled_value(value: object) -> bool:
239+
"""Heuristic: does an extracted value count as "actually filled"?
240+
241+
Treats ``None``, empty strings, whitespace-only strings, and empty
242+
containers as *not* filled. Recursively descends into dicts/lists so a
243+
nested object that contains only nulls is still counted as empty.
244+
"""
245+
if value is None:
246+
return False
247+
if isinstance(value, bool):
248+
return True
249+
if isinstance(value, str):
250+
return value.strip() != ""
251+
if isinstance(value, dict):
252+
return any(SaveHandler._is_filled_value(v) for v in value.values())
253+
if isinstance(value, (list, tuple, set)):
254+
return any(SaveHandler._is_filled_value(v) for v in value)
255+
return True
256+
257+
@staticmethod
258+
def _derive_aggregate_scores(
259+
evaluated_result: DataExtractionResult,
260+
) -> tuple[float, float, float]:
261+
"""Compute ``(entity_score, schema_score, min_extracted_entity_score)``.
262+
263+
Score selection order:
264+
265+
1. **Probabilistic confidence** — when the evaluate step produced
266+
per-field confidence (``total_evaluated_fields_count > 0``), use the
267+
probabilistic ``overall_confidence`` plus the ratio of
268+
above-threshold fields. This is the highest-fidelity signal.
269+
270+
2. **Structural completeness fallback** — when no probabilistic
271+
signal was produced (e.g. reasoning models like ``gpt-5``/``o1``/``o3``
272+
don't return logprobs, and image-only flow has no Content
273+
Understanding signal), but extraction still produced a comparison
274+
table, score by *how much of the schema was actually filled*. This
275+
replaces the old behaviour of falsely emitting ``0%`` for completed
276+
runs that simply lacked logprobs.
277+
278+
3. **Zero** — only when there is literally no extraction data
279+
(failed pipeline / genuinely empty result). Failed processing
280+
continues to surface as ``0`` so the UI consistently renders
281+
``0%`` for failures and genuine zeros.
282+
"""
283+
confidence = evaluated_result.confidence or {}
284+
total_evaluated_fields_count = confidence.get(
285+
"total_evaluated_fields_count", 0
286+
)
287+
comparison_items = (
288+
evaluated_result.comparison_result.items
289+
if evaluated_result.comparison_result is not None
290+
else []
291+
)
292+
293+
# Path 1: probabilistic confidence
294+
if total_evaluated_fields_count > 0 and comparison_items:
295+
zero_count = confidence.get("zero_confidence_fields_count", 0)
296+
schema_score = round(
297+
(len(comparison_items) - zero_count) / len(comparison_items),
298+
3,
299+
)
300+
entity_score = float(confidence.get("overall_confidence") or 0.0)
301+
min_extracted_entity_score = float(
302+
confidence.get("min_extracted_field_confidence") or 0.0
303+
)
304+
return (entity_score, schema_score, min_extracted_entity_score)
305+
306+
# Path 2: structural completeness fallback
307+
if comparison_items:
308+
filled = sum(
309+
1
310+
for item in comparison_items
311+
if SaveHandler._is_filled_value(item.Extracted)
312+
)
313+
ratio = round(filled / len(comparison_items), 3)
314+
return (ratio, ratio, ratio)
315+
316+
# Path 3: nothing to score on
317+
return (0.0, 0.0, 0.0)

src/ContentProcessor/src/libs/utils/azure_credential_utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
from azure.identity import (
2020
AzureCliCredential,
2121
AzureDeveloperCliCredential,
22-
DefaultAzureCredential,
2322
ManagedIdentityCredential,
2423
)
2524
from azure.identity import (
@@ -130,7 +129,11 @@ def get_azure_credential():
130129
logging.info(
131130
"[AUTH] All CLI credentials failed - falling back to DefaultAzureCredential"
132131
)
133-
return DefaultAzureCredential()
132+
raise RuntimeError(
133+
"No Azure authentication available. "
134+
"Use Managed Identity in Azure or run "
135+
"'az login' / 'azd auth login' locally."
136+
)
134137

135138

136139
def get_async_azure_credential():

src/ContentProcessor/src/libs/utils/credential_util.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
from azure.identity import (
2020
AzureCliCredential,
2121
AzureDeveloperCliCredential,
22-
DefaultAzureCredential,
2322
ManagedIdentityCredential,
2423
)
2524
from azure.identity import (
@@ -130,7 +129,11 @@ def get_azure_credential():
130129
logging.info(
131130
"[AUTH] All CLI credentials failed - falling back to DefaultAzureCredential"
132131
)
133-
return DefaultAzureCredential()
132+
raise RuntimeError(
133+
"No Azure authentication available. "
134+
"Use Managed Identity in Azure or run "
135+
"'az login' / 'azd auth login' locally."
136+
)
134137

135138

136139
def get_async_azure_credential():

0 commit comments

Comments
 (0)