6e95517659
* Python: Split type checkers by target (pyright source, 5 checkers on tests/samples) Rework the typing setup along the lines of the 'too many type checkers' approach: - Pyright (strict) is now the sole source-code type checker; mypy is removed from source and its [tool.mypy] block becomes a relaxed profile used only for tests/samples. - Tests are checked by all five checkers (pyright relaxed, mypy, pyrefly, ty, zuban); samples by pyright, pyrefly, and ty. All run in a relaxed/ basic profile so authors aren't forced into over-annotation. - Add pyrightconfig.tests.json and bump sample pyright configs to basic. - Unify test/sample typing onto the same parallel fan-out used by source pyright via run_command_items in task_runner.py. - Make version-conditional imports symmetric: keep or drop the '# type: ignore' on both branches so results match across interpreter versions (local vs CI). - Update SKILL.md, DEV_SETUP.md, and CODING_STANDARD.md for the five gating checkers and pyright on source+tests+samples. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Fix merge regressions from main (typing + runtime) Merging main into the type-checker split branch surfaced regressions that the new five-checker test suite and unit tests caught: Runtime fixes: - anthropic: restore the dropped `cache_read_input_token_count` mapping in _parse_usage_from_anthropic (lost during merge conflict resolution). - gemini: _get_function_calling_mode test helper returned str(enum) ('FunctionCallingConfigMode.AUTO') instead of the enum value ('AUTO'). - openai: _response_id_from_token test helper was an infinite self-recursion; return token['response_id']. - orchestrations: reset output_events per approval iteration so the terminal output assertion counts only the final run. - core: drop a stale duplicate harness test whose message ('non-negative') contradicted the source ('positive'). - purview: import PolicyLocation/PolicyScope/ProtectionScopeActivities/ ExecutionMode used by the processor tests. Type-checker fixes (tests, relaxed profile): - core: pyright/mypy/pyrefly/ty/zuban green-ups across the harness, MCP, observability and types tests. - anthropic/openai: route provider-namespaced UsageDetails keys through a dict cast (extra_items TypedDict unsupported by mypy/ty). - purview: typed model constructors and cache-mock casts. - ag-ui: annotate WorkflowContext[Any, Any] so yield_output accepts test payloads, guard Optional forwarded_props, and ty-ignore intentional bad args. Source pyright (sole source checker) flagged unnecessary ignores newly introduced by merged code in core _tools.py and declarative _declarative_base.py. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Isolate per-package mypy cache in test-typing fan-out The parallel test-typing fan-out runs many mypy processes concurrently, all defaulting to a single shared ./.mypy_cache. Concurrent writes corrupt the cache and mypy aborts with INTERNAL ERROR (intermittently, depending on worker timing) -- which is why CI's Test Typing job failed on a shifting set of packages while a single-package run was fine. Give each mypy invocation an isolated cache dir keyed by its target paths so incremental caching still works per package without races. Other checkers (zuban/pyrefly/ty/pyright) maintain their own caches and are unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Make lab pyright-only on source (drop source mypy) Lab was the last package still running mypy on its source code, requiring mypy-only `# type: ignore` comments that pyright (the sole source checker everywhere else) flags as unnecessary. Align lab with the rest of the monorepo: - Remove the lab source mypy poe tasks (mypy-gaia/lightning/tau2) and the now-dead strict [tool.mypy] config block. - Drop the 'Run lab mypy' CI step; lab source is type-checked by pyright only. Lab tests remain covered by the workspace test-typing fan-out (mypy, pyrefly, ty, zuban, pyright over tests using the relaxed root config). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Fix test-typing regressions from latest main merge A fresh merge from main brought in new test code never run under the five-checker test-typing suite. Green up across the affected packages: - core: narrow Optional span.attributes with 'and' guards in span filters and assert+cast the json.loads(...attributes[...]) reads (test_observability); match the existing as_agent ignore on the protocol-typed fixture (test_clients). - openai: align new streaming tests with the established chat_options dict pattern (ChatOptions TypedDict isn't assignable to dict), route Optional .annotations[0] access through a small _first_annotation helper (mirrors the file's assert-not-None convention), and annotate a mapped ResponseStream. - foundry_hosting: annotate error: dict[str, Any] = body.get(...) or {} (zuban needs the annotation). - foundry: narrow ignores for the live AIProjectClient credential arg (pyrefly) and connections.get_default (zuban) SDK type gaps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * updated pyright version * pyright fix * Python: Fix source typing for pyright 1.1.410 Pyright 1.1.410 tightened several checks. Apply the same source fixes as upstream PR #6275: - anthropic: import AsyncAnthropicBedrock from anthropic.lib.bedrock and AsyncAnthropicVertex from anthropic.lib.vertex (no longer re-exported from the anthropic top-level package -> reportPrivateImportUsage). - core _types.py: cast the transform-hook result to UpdateT (reportAssignmentType). - core _workflows/_events.py: annotate the @contextmanager helper as Generator[None] instead of Iterator[None] (reportDeprecated). - redis: build the combined filter expression with an explicit loop instead of reduce(and_, ...), which pyright could no longer fully type (drops the now unused functools.reduce / operator.and_ imports). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Accept plain-text body in Azure Functions workflow/run endpoint The workflow_orchestrator already accepts plain strings as well as JSON objects via context.get_input(), but the start_workflow_orchestration HTTP handler only accepted JSON and returned 400 for any non-JSON body. This made the functions integration tests that POST text/plain to /api/workflow/run (e.g. test_09_workflow_shared_state) fail consistently with 400 != 202. Fall back to the raw request body (decoded as UTF-8) when the body is not JSON, rejecting only a truly empty body. The JSON path is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
319 lines
12 KiB
Python
319 lines
12 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
"""Integration tests for ContentUnderstandingContextProvider.
|
|
|
|
These tests require a live Azure Content Understanding endpoint.
|
|
Set AZURE_CONTENTUNDERSTANDING_ENDPOINT to enable them.
|
|
|
|
To generate fixtures for unit tests, run these tests with --update-fixtures flag
|
|
and the resulting JSON files will be written to tests/cu/fixtures/.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any, cast
|
|
|
|
import pytest
|
|
|
|
skip_if_cu_integration_tests_disabled = pytest.mark.skipif(
|
|
not os.environ.get("AZURE_CONTENTUNDERSTANDING_ENDPOINT"),
|
|
reason="CU integration tests disabled (AZURE_CONTENTUNDERSTANDING_ENDPOINT not set)",
|
|
)
|
|
|
|
FIXTURES_DIR = Path(__file__).parent / "fixtures"
|
|
|
|
# Shared sample asset — same PDF used by samples and integration tests
|
|
INVOICE_PDF_PATH = Path(__file__).resolve().parents[2] / "samples" / "shared" / "sample_assets" / "invoice.pdf"
|
|
|
|
|
|
@pytest.mark.flaky
|
|
@pytest.mark.integration
|
|
@skip_if_cu_integration_tests_disabled
|
|
async def test_analyze_pdf_binary() -> None:
|
|
"""Analyze a PDF via binary upload and optionally capture fixture."""
|
|
from azure.ai.contentunderstanding.aio import ContentUnderstandingClient
|
|
from azure.identity.aio import DefaultAzureCredential
|
|
|
|
endpoint = os.environ["AZURE_CONTENTUNDERSTANDING_ENDPOINT"]
|
|
analyzer_id = os.environ.get("AZURE_CONTENTUNDERSTANDING_ANALYZER_ID", "prebuilt-documentSearch")
|
|
|
|
pdf_path = INVOICE_PDF_PATH
|
|
assert pdf_path.exists(), f"Test fixture not found: {pdf_path}"
|
|
pdf_bytes = pdf_path.read_bytes()
|
|
|
|
async with DefaultAzureCredential() as credential, ContentUnderstandingClient(endpoint, credential) as client: # pyrefly: ignore[bad-argument-type]
|
|
poller = await client.begin_analyze_binary(
|
|
analyzer_id,
|
|
binary_input=pdf_bytes,
|
|
content_type="application/pdf",
|
|
string_encoding="utf-8",
|
|
)
|
|
result = await poller.result()
|
|
|
|
assert result.contents
|
|
assert result.contents[0].markdown
|
|
assert len(result.contents[0].markdown) > 10
|
|
assert "CONTOSO LTD." in result.contents[0].markdown
|
|
|
|
# Optionally capture fixture
|
|
if os.environ.get("CU_UPDATE_FIXTURES"):
|
|
FIXTURES_DIR.mkdir(exist_ok=True)
|
|
fixture_path = FIXTURES_DIR / "analyze_pdf_result.json"
|
|
fixture_path.write_text(json.dumps(result.as_dict(), indent=2, default=str))
|
|
|
|
|
|
@pytest.mark.flaky
|
|
@pytest.mark.integration
|
|
@skip_if_cu_integration_tests_disabled
|
|
async def test_before_run_e2e() -> None:
|
|
"""End-to-end test: Content.from_data → before_run → state populated."""
|
|
from agent_framework import Content, Message, SessionContext
|
|
from agent_framework._sessions import AgentSession
|
|
from azure.identity.aio import DefaultAzureCredential
|
|
|
|
from agent_framework_azure_contentunderstanding import ContentUnderstandingContextProvider
|
|
|
|
endpoint = os.environ["AZURE_CONTENTUNDERSTANDING_ENDPOINT"]
|
|
|
|
pdf_path = INVOICE_PDF_PATH
|
|
assert pdf_path.exists(), f"Test fixture not found: {pdf_path}"
|
|
pdf_bytes = pdf_path.read_bytes()
|
|
|
|
async with DefaultAzureCredential() as credential:
|
|
cu = ContentUnderstandingContextProvider(
|
|
endpoint=endpoint,
|
|
credential=credential, # pyrefly: ignore[bad-argument-type]
|
|
max_wait=None, # wait until analysis completes (no background deferral)
|
|
)
|
|
async with cu:
|
|
msg = Message(
|
|
role="user",
|
|
contents=[
|
|
Content.from_text("What's in this document?"),
|
|
Content.from_data(
|
|
pdf_bytes,
|
|
"application/pdf",
|
|
additional_properties={"filename": "invoice.pdf"},
|
|
),
|
|
],
|
|
)
|
|
context = SessionContext(input_messages=[msg])
|
|
state: dict[str, object] = {}
|
|
session = AgentSession()
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
await cu.before_run(agent=MagicMock(), session=session, context=context, state=state)
|
|
|
|
docs = cast("dict[str, Any]", state.get("documents", {}))
|
|
assert isinstance(docs, dict)
|
|
assert "invoice.pdf" in docs
|
|
doc_entry = docs["invoice.pdf"]
|
|
assert doc_entry["status"] == "ready"
|
|
# ``result`` is now the rendered string from ``to_llm_input``.
|
|
rendered = doc_entry["result"]
|
|
assert isinstance(rendered, str)
|
|
assert len(rendered) > 10
|
|
assert "source: invoice.pdf" in rendered
|
|
assert "CONTOSO LTD." in rendered
|
|
|
|
|
|
# Raw GitHub URL for a public invoice PDF from the CU samples repo
|
|
_INVOICE_PDF_URL = (
|
|
"https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/document/invoice.pdf"
|
|
)
|
|
|
|
|
|
@pytest.mark.flaky
|
|
@pytest.mark.integration
|
|
@skip_if_cu_integration_tests_disabled
|
|
async def test_before_run_uri_content() -> None:
|
|
"""End-to-end test: Content.from_uri with an external URL → before_run → state populated.
|
|
|
|
Verifies that CU can analyze a file referenced by URL (not base64 data).
|
|
Uses a public invoice PDF from the Azure CU samples repository.
|
|
"""
|
|
from agent_framework import Content, Message, SessionContext
|
|
from agent_framework._sessions import AgentSession
|
|
from azure.identity.aio import DefaultAzureCredential
|
|
|
|
from agent_framework_azure_contentunderstanding import ContentUnderstandingContextProvider
|
|
|
|
endpoint = os.environ["AZURE_CONTENTUNDERSTANDING_ENDPOINT"]
|
|
|
|
async with DefaultAzureCredential() as credential:
|
|
cu = ContentUnderstandingContextProvider(
|
|
endpoint=endpoint,
|
|
credential=credential, # pyrefly: ignore[bad-argument-type]
|
|
max_wait=None, # wait until analysis completes (no background deferral)
|
|
)
|
|
async with cu:
|
|
msg = Message(
|
|
role="user",
|
|
contents=[
|
|
Content.from_text("What's on this invoice?"),
|
|
Content.from_uri(
|
|
uri=_INVOICE_PDF_URL,
|
|
media_type="application/pdf",
|
|
additional_properties={"filename": "invoice.pdf"},
|
|
),
|
|
],
|
|
)
|
|
context = SessionContext(input_messages=[msg])
|
|
state: dict[str, object] = {}
|
|
session = AgentSession()
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
await cu.before_run(agent=MagicMock(), session=session, context=context, state=state)
|
|
|
|
docs = cast("dict[str, Any]", state.get("documents", {}))
|
|
assert isinstance(docs, dict)
|
|
assert "invoice.pdf" in docs
|
|
|
|
doc_entry = docs["invoice.pdf"]
|
|
assert doc_entry["status"] == "ready"
|
|
rendered = doc_entry["result"]
|
|
assert isinstance(rendered, str)
|
|
assert len(rendered) > 10
|
|
assert "source: invoice.pdf" in rendered
|
|
assert "CONTOSO LTD." in rendered
|
|
|
|
|
|
@pytest.mark.flaky
|
|
@pytest.mark.integration
|
|
@skip_if_cu_integration_tests_disabled
|
|
async def test_before_run_data_uri_content() -> None:
|
|
"""End-to-end test: Content.from_uri with a base64 data URI → before_run → state populated.
|
|
|
|
Verifies that CU can analyze a file embedded as a data URI (data:application/pdf;base64,...).
|
|
This tests the data URI path: from_uri with "data:" prefix → type="data" → begin_analyze_binary.
|
|
"""
|
|
import base64
|
|
|
|
from agent_framework import Content, Message, SessionContext
|
|
from agent_framework._sessions import AgentSession
|
|
from azure.identity.aio import DefaultAzureCredential
|
|
|
|
from agent_framework_azure_contentunderstanding import ContentUnderstandingContextProvider
|
|
|
|
endpoint = os.environ["AZURE_CONTENTUNDERSTANDING_ENDPOINT"]
|
|
|
|
pdf_path = INVOICE_PDF_PATH
|
|
assert pdf_path.exists(), f"Test fixture not found: {pdf_path}"
|
|
pdf_bytes = pdf_path.read_bytes()
|
|
b64 = base64.b64encode(pdf_bytes).decode("ascii")
|
|
data_uri = f"data:application/pdf;base64,{b64}"
|
|
|
|
async with DefaultAzureCredential() as credential:
|
|
cu = ContentUnderstandingContextProvider(
|
|
endpoint=endpoint,
|
|
credential=credential, # pyrefly: ignore[bad-argument-type]
|
|
max_wait=None, # wait until analysis completes
|
|
)
|
|
async with cu:
|
|
msg = Message(
|
|
role="user",
|
|
contents=[
|
|
Content.from_text("What's on this invoice?"),
|
|
Content.from_uri(
|
|
uri=data_uri,
|
|
media_type="application/pdf",
|
|
additional_properties={"filename": "invoice_b64.pdf"},
|
|
),
|
|
],
|
|
)
|
|
context = SessionContext(input_messages=[msg])
|
|
state: dict[str, object] = {}
|
|
session = AgentSession()
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
await cu.before_run(agent=MagicMock(), session=session, context=context, state=state)
|
|
|
|
docs = cast("dict[str, Any]", state.get("documents", {}))
|
|
assert isinstance(docs, dict)
|
|
assert "invoice_b64.pdf" in docs
|
|
|
|
doc_entry = docs["invoice_b64.pdf"]
|
|
assert doc_entry["status"] == "ready"
|
|
rendered = doc_entry["result"]
|
|
assert isinstance(rendered, str)
|
|
assert len(rendered) > 10
|
|
assert "source: invoice_b64.pdf" in rendered
|
|
assert "CONTOSO LTD." in rendered
|
|
|
|
|
|
@pytest.mark.flaky
|
|
@pytest.mark.integration
|
|
@skip_if_cu_integration_tests_disabled
|
|
async def test_before_run_background_analysis() -> None:
|
|
"""End-to-end test: max_wait timeout → background analysis → resolved on next turn.
|
|
|
|
Uses a short max_wait (0.5s) so CU analysis is deferred to background.
|
|
Then waits for analysis to complete and calls before_run again to verify
|
|
the background task resolves and the document becomes ready.
|
|
"""
|
|
import asyncio
|
|
|
|
from agent_framework import Content, Message, SessionContext
|
|
from agent_framework._sessions import AgentSession
|
|
from azure.identity.aio import DefaultAzureCredential
|
|
|
|
from agent_framework_azure_contentunderstanding import ContentUnderstandingContextProvider
|
|
|
|
endpoint = os.environ["AZURE_CONTENTUNDERSTANDING_ENDPOINT"]
|
|
|
|
async with DefaultAzureCredential() as credential:
|
|
cu = ContentUnderstandingContextProvider(
|
|
endpoint=endpoint,
|
|
credential=credential, # pyrefly: ignore[bad-argument-type]
|
|
max_wait=0.5, # short timeout to force background deferral
|
|
)
|
|
async with cu:
|
|
# Turn 1: upload file — should time out and defer to background
|
|
msg = Message(
|
|
role="user",
|
|
contents=[
|
|
Content.from_text("What's on this invoice?"),
|
|
Content.from_uri(
|
|
uri=_INVOICE_PDF_URL,
|
|
media_type="application/pdf",
|
|
additional_properties={"filename": "invoice.pdf"},
|
|
),
|
|
],
|
|
)
|
|
context = SessionContext(input_messages=[msg])
|
|
state: dict[str, object] = {}
|
|
session = AgentSession()
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
await cu.before_run(agent=MagicMock(), session=session, context=context, state=state)
|
|
|
|
docs = cast("dict[str, Any]", state.get("documents", {}))
|
|
assert isinstance(docs, dict)
|
|
assert "invoice.pdf" in docs
|
|
assert docs["invoice.pdf"]["status"] == "analyzing", (
|
|
f"Expected 'analyzing' but got '{docs['invoice.pdf']['status']}' — "
|
|
"CU responded too fast for the 0.5s timeout"
|
|
)
|
|
assert docs["invoice.pdf"]["result"] is None
|
|
|
|
# Wait for background analysis to complete
|
|
await asyncio.sleep(30)
|
|
|
|
# Turn 2: no new files — should resolve the background task
|
|
msg2 = Message(role="user", contents=[Content.from_text("Is it ready?")])
|
|
context2 = SessionContext(input_messages=[msg2])
|
|
|
|
await cu.before_run(agent=MagicMock(), session=session, context=context2, state=state)
|
|
|
|
assert docs["invoice.pdf"]["status"] == "ready"
|
|
rendered = docs["invoice.pdf"]["result"]
|
|
assert isinstance(rendered, str)
|
|
assert "CONTOSO LTD." in rendered
|