Python: chore(python): improve dependency range automation (#4343)

* chore(python): improve dependency range automation

- tighten dependency bounds and coding standards guidance\n- add dependency range validation workflow, reporting, and issue automation\n- update related tests and dependency pins for compatibility

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* updated text and pyarrow

* new lock

* fixed workflow

* updated deps

* fix tiktoken

* chore(python): refine dependency validation workflows

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(python): add high-level dependency validation comments

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* WIP

* added additional comments and excludes

* added dev dependency handling and workflow and updates to package ranges

* added readme and simplified commands

* fix markers

* chore(python): address dependency review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Tighten dependency bounds, remove stale overrides, restore Python 3.10 support

- Apply dependency bound policy across all packages: stable >=1.0 deps use
  >=floor,<next_major; pre-1.0/prerelease deps use validated hard-bounded ranges
- Remove stale root tool.uv.override-dependencies (uvicorn, websockets, grpcio)
- Lower github_copilot requires-python to >=3.10 with github-copilot-sdk gated
  behind python_version >= 3.11 marker; import raises ImportError on 3.10
- Skip github_copilot pyright/mypy/test tasks on Python <3.11
- Use version-conditional pyrightconfig for samples on Python 3.10
- Add compatibility fix in core responses client for older openai typed dicts
- Normalize uv.lock prerelease mode and refresh dev dependencies
- Update CODING_STANDARD.md, DEV_SETUP.md, and package management skill docs

Closes #902

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* small tweaks

* add note in workflow

* fix workflows and several versions

* fix duplicate

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-03-13 13:32:37 +01:00
committed by GitHub
Unverified
parent 67b0282813
commit 50fdcbaf57
61 changed files with 5500 additions and 779 deletions
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: RUF070, RUF100
from __future__ import annotations
import asyncio
@@ -665,7 +665,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
if output_format:
tool["output_format"] = output_format
if model:
tool["model"] = model
tool["model"] = model # type: ignore
if quality:
tool["quality"] = quality
if partial_images is not None:
+8 -16
View File
@@ -24,19 +24,19 @@ classifiers = [
]
dependencies = [
# utilities
"typing-extensions",
"typing-extensions>=4.15.0,<5",
"pydantic>=2,<3",
"python-dotenv>=1,<2",
# telemetry
"opentelemetry-api>=1.39.0",
"opentelemetry-sdk>=1.39.0",
"opentelemetry-semantic-conventions-ai>=0.4.13",
"opentelemetry-api>=1.39.0,<2",
"opentelemetry-sdk>=1.39.0,<2",
"opentelemetry-semantic-conventions-ai>=0.4.13,<0.4.14",
# connectors and functions
"openai>=1.99.0",
"openai>=1.99.0,<3",
"azure-identity>=1,<2",
"azure-ai-projects>=2.0.0,<3.0",
"mcp[ws]>=1.24.0,<2",
"packaging>=24.1",
"packaging>=24.1,<25",
]
[project.optional-dependencies]
@@ -76,15 +76,7 @@ environments = [
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = [
'tests',
'packages/core/tests',
'packages/a2a/tests',
'packages/azure-ai/tests',
'packages/copilotstudio/tests',
'packages/mem0/tests',
'packages/runtime/tests'
]
testpaths = ['tests']
addopts = "-ra -q -r fEX"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
@@ -131,7 +123,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework"
test = "pytest -m \"not integration\" --cov=agent_framework --cov-report=term-missing:skip-covered -n auto --dist worksteal tests"
test = 'pytest -m "not integration" --cov=agent_framework --cov-report=term-missing:skip-covered -n auto --dist worksteal tests'
[tool.flit.module]
name = "agent_framework"
@@ -0,0 +1,159 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import os
from unittest.mock import AsyncMock, MagicMock
import pytest
from openai.types import CreateEmbeddingResponse
from openai.types import Embedding as OpenAIEmbedding
from openai.types.create_embedding_response import Usage
from agent_framework.azure import AzureOpenAIEmbeddingClient
from agent_framework.openai import OpenAIEmbeddingOptions
def _make_openai_response(
embeddings: list[list[float]],
model: str = "text-embedding-3-small",
prompt_tokens: int = 5,
total_tokens: int = 5,
) -> CreateEmbeddingResponse:
"""Helper to create a mock OpenAI embeddings response."""
data = [OpenAIEmbedding(embedding=emb, index=i, object="embedding") for i, emb in enumerate(embeddings)]
return CreateEmbeddingResponse(
data=data,
model=model,
object="list",
usage=Usage(prompt_tokens=prompt_tokens, total_tokens=total_tokens),
)
@pytest.fixture
def azure_embedding_unit_test_env(monkeypatch: pytest.MonkeyPatch) -> None:
"""Clear ambient Azure OpenAI embedding env vars for deterministic unit tests."""
for key in (
"AZURE_OPENAI_ENDPOINT",
"AZURE_OPENAI_API_KEY",
"AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME",
"AZURE_OPENAI_BASE_URL",
"AZURE_OPENAI_TOKEN_ENDPOINT",
):
monkeypatch.delenv(key, raising=False)
def test_azure_construction_with_deployment_name(azure_embedding_unit_test_env: None) -> None:
client = AzureOpenAIEmbeddingClient(
deployment_name="text-embedding-3-small",
api_key="test-key",
endpoint="https://test.openai.azure.com/",
)
assert client.model_id == "text-embedding-3-small"
def test_azure_construction_with_existing_client(azure_embedding_unit_test_env: None) -> None:
mock_client = MagicMock()
client = AzureOpenAIEmbeddingClient(
deployment_name="my-deployment",
async_client=mock_client,
)
assert client.model_id == "my-deployment"
assert client.client is mock_client
def test_azure_construction_missing_deployment_name_raises(azure_embedding_unit_test_env: None) -> None:
with pytest.raises(ValueError, match="deployment name is required"):
AzureOpenAIEmbeddingClient(
api_key="test-key",
endpoint="https://test.openai.azure.com/",
)
def test_azure_construction_missing_credentials_raises(azure_embedding_unit_test_env: None) -> None:
with pytest.raises(ValueError, match="api_key, credential, or a client"):
AzureOpenAIEmbeddingClient(
deployment_name="test",
endpoint="https://test.openai.azure.com/",
)
async def test_azure_get_embeddings(azure_embedding_unit_test_env: None) -> None:
mock_response = _make_openai_response(
embeddings=[[0.1, 0.2]],
)
mock_async_client = MagicMock()
mock_async_client.embeddings = MagicMock()
mock_async_client.embeddings.create = AsyncMock(return_value=mock_response)
client = AzureOpenAIEmbeddingClient(
deployment_name="text-embedding-3-small",
async_client=mock_async_client,
)
result = await client.get_embeddings(["hello"])
assert len(result) == 1
assert result[0].vector == [0.1, 0.2]
def test_azure_otel_provider_name(azure_embedding_unit_test_env: None) -> None:
mock_client = MagicMock()
client = AzureOpenAIEmbeddingClient(
deployment_name="test",
async_client=mock_client,
)
assert client.OTEL_PROVIDER_NAME == "azure.ai.openai"
skip_if_azure_openai_integration_tests_disabled = pytest.mark.skipif(
not os.getenv("AZURE_OPENAI_ENDPOINT")
or (not os.getenv("AZURE_OPENAI_API_KEY") and not os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME")),
reason="No Azure OpenAI credentials provided; skipping integration tests.",
)
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
async def test_integration_azure_openai_get_embeddings() -> None:
"""End-to-end test of Azure OpenAI embedding generation."""
client = AzureOpenAIEmbeddingClient()
result = await client.get_embeddings(["hello world"])
assert len(result) == 1
assert isinstance(result[0].vector, list)
assert len(result[0].vector) > 0
assert all(isinstance(v, float) for v in result[0].vector)
assert result[0].model_id is not None
assert result.usage is not None
assert result.usage["input_token_count"] > 0
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
async def test_integration_azure_openai_get_embeddings_multiple() -> None:
"""Test Azure OpenAI embedding generation for multiple inputs."""
client = AzureOpenAIEmbeddingClient()
result = await client.get_embeddings(["hello", "world", "test"])
assert len(result) == 3
dims = [len(e.vector) for e in result]
assert all(d == dims[0] for d in dims)
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
async def test_integration_azure_openai_get_embeddings_with_dimensions() -> None:
"""Test Azure OpenAI embedding generation with custom dimensions."""
client = AzureOpenAIEmbeddingClient()
options: OpenAIEmbeddingOptions = {"dimensions": 256}
result = await client.get_embeddings(["hello world"], options=options)
assert len(result) == 1
assert len(result[0].vector) == 256
@@ -10,7 +10,6 @@ from openai.types import CreateEmbeddingResponse
from openai.types import Embedding as OpenAIEmbedding
from openai.types.create_embedding_response import Usage
from agent_framework.azure import AzureOpenAIEmbeddingClient
from agent_framework.openai import (
OpenAIEmbeddingClient,
OpenAIEmbeddingOptions,
@@ -190,73 +189,6 @@ async def test_openai_empty_values_returns_empty(openai_unit_test_env: None) ->
client.client.embeddings.create.assert_not_called()
# --- Azure OpenAI unit tests ---
def test_azure_construction_with_deployment_name() -> None:
client = AzureOpenAIEmbeddingClient(
deployment_name="text-embedding-3-small",
api_key="test-key",
endpoint="https://test.openai.azure.com/",
)
assert client.model_id == "text-embedding-3-small"
def test_azure_construction_with_existing_client() -> None:
mock_client = MagicMock()
client = AzureOpenAIEmbeddingClient(
deployment_name="my-deployment",
async_client=mock_client,
)
assert client.model_id == "my-deployment"
assert client.client is mock_client
def test_azure_construction_missing_deployment_name_raises(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME", raising=False)
with pytest.raises(ValueError, match="deployment name is required"):
AzureOpenAIEmbeddingClient(
api_key="test-key",
endpoint="https://test.openai.azure.com/",
)
def test_azure_construction_missing_credentials_raises() -> None:
with pytest.raises(ValueError, match="api_key, credential, or a client"):
AzureOpenAIEmbeddingClient(
deployment_name="test",
endpoint="https://test.openai.azure.com/",
)
async def test_azure_get_embeddings() -> None:
mock_response = _make_openai_response(
embeddings=[[0.1, 0.2]],
)
mock_async_client = MagicMock()
mock_async_client.embeddings = MagicMock()
mock_async_client.embeddings.create = AsyncMock(return_value=mock_response)
client = AzureOpenAIEmbeddingClient(
deployment_name="text-embedding-3-small",
async_client=mock_async_client,
)
result = await client.get_embeddings(["hello"])
assert len(result) == 1
assert result[0].vector == [0.1, 0.2]
def test_azure_otel_provider_name() -> None:
mock_client = MagicMock()
client = AzureOpenAIEmbeddingClient(
deployment_name="test",
async_client=mock_client,
)
assert client.OTEL_PROVIDER_NAME == "azure.ai.openai"
# --- Integration tests ---
skip_if_openai_integration_tests_disabled = pytest.mark.skipif(
@@ -264,12 +196,6 @@ skip_if_openai_integration_tests_disabled = pytest.mark.skipif(
reason="No real OPENAI_API_KEY provided; skipping integration tests.",
)
skip_if_azure_openai_integration_tests_disabled = pytest.mark.skipif(
not os.getenv("AZURE_OPENAI_ENDPOINT")
or (not os.getenv("AZURE_OPENAI_API_KEY") and not os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME")),
reason="No Azure OpenAI credentials provided; skipping integration tests.",
)
@skip_if_openai_integration_tests_disabled
@pytest.mark.flaky
@@ -315,49 +241,3 @@ async def test_integration_openai_get_embeddings_with_dimensions() -> None:
assert len(result) == 1
assert len(result[0].vector) == 256
@skip_if_azure_openai_integration_tests_disabled
@pytest.mark.flaky
@pytest.mark.integration
async def test_integration_azure_openai_get_embeddings() -> None:
"""End-to-end test of Azure OpenAI embedding generation."""
client = AzureOpenAIEmbeddingClient()
result = await client.get_embeddings(["hello world"])
assert len(result) == 1
assert isinstance(result[0].vector, list)
assert len(result[0].vector) > 0
assert all(isinstance(v, float) for v in result[0].vector)
assert result[0].model_id is not None
assert result.usage is not None
assert result.usage["input_token_count"] > 0
@skip_if_azure_openai_integration_tests_disabled
@pytest.mark.flaky
@pytest.mark.integration
async def test_integration_azure_openai_get_embeddings_multiple() -> None:
"""Test Azure OpenAI embedding generation for multiple inputs."""
client = AzureOpenAIEmbeddingClient()
result = await client.get_embeddings(["hello", "world", "test"])
assert len(result) == 3
dims = [len(e.vector) for e in result]
assert all(d == dims[0] for d in dims)
@skip_if_azure_openai_integration_tests_disabled
@pytest.mark.flaky
@pytest.mark.integration
async def test_integration_azure_openai_get_embeddings_with_dimensions() -> None:
"""Test Azure OpenAI embedding generation with custom dimensions."""
client = AzureOpenAIEmbeddingClient()
options: OpenAIEmbeddingOptions = {"dimensions": 256}
result = await client.get_embeddings(["hello world"], options=options)
assert len(result) == 1
assert len(result[0].vector) == 256
@@ -1876,6 +1876,19 @@ def test_prepare_tools_for_openai_with_image_generation_options() -> None:
assert image_tool["quality"] == "high"
def test_prepare_tools_for_openai_with_custom_image_generation_model() -> None:
"""Test image generation tool conversion with a custom model string."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
tool = OpenAIResponsesClient.get_image_generation_tool(model="custom-image-model")
resp_tools = client._prepare_tools_for_openai([tool])
assert len(resp_tools) == 1
image_tool = resp_tools[0]
assert image_tool["type"] == "image_generation"
assert image_tool["model"] == "custom-image-model"
def test_parse_chunk_from_openai_with_mcp_approval_request() -> None:
"""Test that a streaming mcp_approval_request event is parsed into FunctionApprovalRequestContent."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")