Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8d62f5b
Refactor DocumentEntry model and update result handling
changjian-wang May 13, 2026
51be00b
Potential fix for pull request finding
changjian-wang May 13, 2026
6419e4c
Merge branch 'main' into changjian-wang/python-cu-to-llm-input-adoption
changjian-wang May 13, 2026
0ab5383
Merge branch 'main' into changjian-wang/python-cu-to-llm-input-adoption
changjian-wang May 13, 2026
e6c25be
Merge branch 'main' into changjian-wang/python-cu-to-llm-input-adoption
changjian-wang May 14, 2026
420c336
Add test to ensure page markers are preserved in LLM input
changjian-wang May 14, 2026
cd9cdfb
Merge remote-tracking branch 'upstream/main' into changjian-wang/pyth…
changjian-wang May 21, 2026
b62d92b
fix(cu-context-provider): scope LLMStats telemetry filter to rai_warn…
May 21, 2026
9dbc43c
Merge branch 'main' into changjian-wang/python-cu-to-llm-input-adoption
changjian-wang May 21, 2026
63513a3
Sync uv.lock with azure-ai-contentunderstanding>=1.2.0b1 dependency bump
wangchangjian1130 May 21, 2026
3bb10de
Merge remote-tracking branch 'upstream/main' into changjian-wang/pyth…
changjian-wang May 22, 2026
1ec70d5
Merge branch 'main' into changjian-wang/python-cu-to-llm-input-adoption
changjian-wang May 25, 2026
037325f
Merge branch 'main' into changjian-wang/python-cu-to-llm-input-adoption
changjian-wang May 28, 2026
5ca50dc
Merge branch 'main' into changjian-wang/python-cu-to-llm-input-adoption
changjian-wang May 28, 2026
d543a1f
Merge branch 'main' into changjian-wang/python-cu-to-llm-input-adoption
changjian-wang May 29, 2026
120cd82
Python: Drop search_payload/include_fields, single to_llm_input rende…
changjian-wang May 29, 2026
5bbd066
Merge branch 'main' into changjian-wang/python-cu-to-llm-input-adoption
changjian-wang May 29, 2026
c57c386
Python: Adopt SDK 1.2.0b2 LLMStats filtering, drop local workaround (…
changjian-wang Jun 12, 2026
1e858fd
Merge branch 'main' into changjian-wang/python-cu-to-llm-input-adoption
changjian-wang Jun 12, 2026
042fab8
Merge branch 'main' into changjian-wang/python-cu-to-llm-input-adoption
changjian-wang Jun 15, 2026
8905507
Merge branch 'main' into changjian-wang/python-cu-to-llm-input-adoption
changjian-wang Jun 15, 2026
dc98bbc
Merge branch 'main' into changjian-wang/python-cu-to-llm-input-adoption
changjian-wang Jun 15, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import asyncio
import json
import logging
import re
import sys
import time
from datetime import datetime, timezone
Expand All @@ -28,6 +29,7 @@
)
from agent_framework._sessions import AgentSession
from agent_framework._settings import load_settings
from azure.ai.contentunderstanding import to_llm_input
from azure.ai.contentunderstanding.aio import ContentUnderstandingClient
from azure.ai.contentunderstanding.models import AnalysisInput, AnalysisResult
from azure.core.credentials import AzureKeyCredential
Expand All @@ -39,7 +41,6 @@
from ._detection import (
detect_and_strip_files,
)
from ._extraction import extract_sections, format_result
from ._models import AnalysisSection, DocumentEntry, DocumentStatus, FileSearchConfig

if sys.version_info >= (3, 11):
Expand All @@ -59,6 +60,27 @@
}
DEFAULT_ANALYZER: str = "prebuilt-documentSearch"

# Matches the leading YAML front-matter block emitted by ``to_llm_input``.
# A rendered text with no markdown body (e.g. when the CU result has empty
# ``markdown`` and no fields) is recognised by an empty tail after this match.
# Accept both LF and CRLF line endings so body detection works cross-platform.
_FRONT_MATTER_RE: re.Pattern[str] = re.compile(r"\A---\r?\n.*?\r?\n---(?:\r?\n|\Z)", flags=re.DOTALL)


def _has_renderable_body(text: str) -> bool:
"""Return True when ``text`` has any non-whitespace content beyond YAML front matter.

Used to skip ``file_search`` uploads when CU produced a result with no
markdown content — uploading a front-matter-only stub would pollute the
vector store without giving the LLM anything searchable.
"""
if not text:
return False
match = _FRONT_MATTER_RE.match(text)
if match is None:
return bool(text.strip())
return bool(text[match.end() :].strip())


class ContentUnderstandingSettings(TypedDict, total=False):
"""Settings for ContentUnderstandingContextProvider with auto-loading from environment.
Expand Down Expand Up @@ -263,8 +285,8 @@ async def before_run(
pending_tokens: dict[str, dict[str, str]] = state.setdefault("_pending_tokens", {})
pending_uploads: list[tuple[str, DocumentEntry]] = state.setdefault("_pending_uploads", [])

# 1. Resolve pending background analyses via continuation tokens
await self._resolve_pending_tokens(pending_tokens, pending_uploads, documents, context)
# Resolve pending Content Understanding analysis from its continuation tokens
await self._resolve_pending_analysis(pending_tokens, pending_uploads, documents, context)

# 1b. Upload any documents that completed in the background (file_search mode)
if pending_uploads:
Expand Down Expand Up @@ -415,7 +437,7 @@ async def before_run(
context.extend_messages(
self,
[
Message(role="user", contents=[format_result(entry["filename"], entry["result"])]),
Message(role="user", contents=[entry["result"] or ""]),
],
)
context.extend_messages(
Expand All @@ -428,7 +450,7 @@ async def before_run(
f"The user just uploaded '{entry['filename']}'."
" It has been analyzed using Azure Content Understanding."
" The document content (markdown) and extracted fields"
" (JSON) are provided above."
" (YAML front matter) are provided above."
" If the user's question is ambiguous,"
" prioritize this most recently uploaded document."
" Use specific field values and cite page numbers"
Expand Down Expand Up @@ -561,7 +583,7 @@ async def _analyze_file(

# Analysis completed within timeout
analysis_duration = round(time.monotonic() - t0, 2)
extracted = self._extract_sections(result)
rendered = self._render_for_llm(result, filename)
logger.info("Analyzed '%s' with analyzer '%s' in %.1fs.", filename, resolved_analyzer, analysis_duration)
return DocumentEntry(
status=DocumentStatus.READY,
Expand All @@ -571,7 +593,7 @@ async def _analyze_file(
analyzed_at=datetime.now(tz=timezone.utc).isoformat(),
analysis_duration_s=analysis_duration,
upload_duration_s=None,
result=extracted,
result=rendered,
error=None,
)

Expand All @@ -596,10 +618,10 @@ async def _analyze_file(
)

# ------------------------------------------------------------------
# Pending Token Resolution
# Pending Analysis Resolution
# ------------------------------------------------------------------

async def _resolve_pending_tokens(
async def _resolve_pending_analysis(
self,
pending_tokens: dict[str, dict[str, str]],
pending_uploads: list[tuple[str, DocumentEntry]],
Expand Down Expand Up @@ -658,10 +680,10 @@ async def _resolve_pending_tokens(
continue

completed_keys.append(doc_key)
extracted = self._extract_sections(result) # pyright: ignore[reportUnknownArgumentType]
rendered = self._render_for_llm(result, entry["filename"]) # pyright: ignore[reportUnknownArgumentType]
entry["status"] = DocumentStatus.READY
entry["analyzed_at"] = datetime.now(tz=timezone.utc).isoformat()
entry["result"] = extracted
Comment thread
changjian-wang marked this conversation as resolved.
entry["result"] = rendered
entry["error"] = None
logger.info("Background analysis of '%s' completed.", entry["filename"])

Expand All @@ -672,7 +694,7 @@ async def _resolve_pending_tokens(
context.extend_messages(
self,
[
Message(role="user", contents=[format_result(entry["filename"], extracted)]),
Message(role="user", contents=[rendered]),
],
)
context.extend_messages(
Expand Down Expand Up @@ -708,11 +730,36 @@ async def _resolve_pending_tokens(
del pending_tokens[key]

# ------------------------------------------------------------------
# Output Extraction & Formatting (delegates to _extraction module)
# LLM Input Rendering (delegates to azure.ai.contentunderstanding.to_llm_input)
# ------------------------------------------------------------------

def _extract_sections(self, result: AnalysisResult) -> dict[str, object]:
return extract_sections(result, self.output_sections)
def _render_for_llm(
self,
result: AnalysisResult,
filename: str,
) -> str:
"""Render a CU ``AnalysisResult`` into LLM-friendly text.

Maps the MAF ``output_sections`` list to ``to_llm_input`` kwargs:

- ``"markdown" in output_sections`` -> ``include_markdown=True``
- ``"fields" in output_sections`` -> ``include_fields=True``

Args:
result: The CU analysis result.
filename: Document filename, surfaced to the LLM via the
``source`` front matter key.

Returns:
A YAML-front-matter-prefixed text block ready for direct LLM
consumption or vector store upload.
"""
return to_llm_input(
result,
include_markdown="markdown" in self.output_sections,
include_fields="fields" in self.output_sections,
metadata={"source": filename},
)

# ------------------------------------------------------------------
# Tool Registration
Expand Down Expand Up @@ -801,18 +848,18 @@ async def _upload_to_vector_store(
if not result:
return False

# Upload the full formatted content (markdown + fields + segments),
# not just raw markdown — consistent with what non-file_search mode injects.
formatted = format_result(entry["filename"], result)
if not formatted:
if not _has_renderable_body(result):
# Empty CU result (e.g. blank markdown, no fields) — skip the
# upload so the vector store stays clean. The DocumentEntry still
# records the front-matter-only ``result`` so callers can introspect.
return False

entry["status"] = DocumentStatus.UPLOADING
t0 = time.monotonic()

try:
upload_coro = self.file_search.backend.upload_file(
self.file_search.vector_store_id, f"{doc_key}.md", formatted.encode("utf-8")
self.file_search.vector_store_id, f"{doc_key}.md", result.encode("utf-8")
)
file_id = await asyncio.wait_for(upload_coro, timeout=timeout)
upload_duration = round(time.monotonic() - t0, 2)
Expand All @@ -822,7 +869,7 @@ async def _upload_to_vector_store(
self._all_uploaded_file_ids.append(file_id)
entry["status"] = DocumentStatus.READY
entry["upload_duration_s"] = upload_duration
logger.info("Uploaded '%s' to vector store in %.1fs (%s bytes).", doc_key, upload_duration, len(formatted))
logger.info("Uploaded '%s' to vector store in %.1fs (%s bytes).", doc_key, upload_duration, len(result))
return True

except asyncio.TimeoutError:
Expand Down
Loading
Loading