diff --git a/libraries/microsoft-agents-a365-observability-hosting/pyproject.toml b/libraries/microsoft-agents-a365-observability-hosting/pyproject.toml index 40f07a11..c3cc34d1 100644 --- a/libraries/microsoft-agents-a365-observability-hosting/pyproject.toml +++ b/libraries/microsoft-agents-a365-observability-hosting/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ license = {text = "MIT"} keywords = ["observability", "telemetry", "tracing", "opentelemetry", "monitoring", "ai", "agents", "hosting"] dependencies = [ - "microsoft-agents-hosting-core >= 0.4.0, < 0.6.0", + "microsoft-agents-hosting-core >= 0.4.0", "microsoft-agents-a365-observability-core >= 0.0.0", "opentelemetry-api >= 1.36.0", ] diff --git a/libraries/microsoft-agents-a365-tooling-extensions-agentframework/pyproject.toml b/libraries/microsoft-agents-a365-tooling-extensions-agentframework/pyproject.toml index 030b08b0..e155ce6d 100644 --- a/libraries/microsoft-agents-a365-tooling-extensions-agentframework/pyproject.toml +++ b/libraries/microsoft-agents-a365-tooling-extensions-agentframework/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ license = {text = "MIT"} dependencies = [ "microsoft-agents-a365-tooling >= 0.0.0", - "microsoft-agents-hosting-core >= 0.4.0, < 0.6.0", + "microsoft-agents-hosting-core >= 0.4.0", "agent-framework-azure-ai >= 1.0.0b251114", "azure-identity >= 1.12.0", "typing-extensions >= 4.0.0", diff --git a/libraries/microsoft-agents-a365-tooling/pyproject.toml b/libraries/microsoft-agents-a365-tooling/pyproject.toml index 77fbceec..e17f8d09 100644 --- a/libraries/microsoft-agents-a365-tooling/pyproject.toml +++ b/libraries/microsoft-agents-a365-tooling/pyproject.toml @@ -25,7 +25,7 @@ license = {text = "MIT"} dependencies = [ "pydantic >= 2.0.0", "typing-extensions >= 4.0.0", - "microsoft-agents-hosting-core >= 0.4.0, < 0.6.0", + "microsoft-agents-hosting-core >= 0.4.0", ] [project.urls] diff --git a/tests/test_dependency_constraints.py b/tests/test_dependency_constraints.py new file mode 100644 index 00000000..af88b985 --- /dev/null +++ b/tests/test_dependency_constraints.py @@ -0,0 +1,251 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +Tests for validating dependency version constraints across all packages. + +This test ensures that upper bound version constraints (e.g., < 0.6.0) on dependencies +do not become incompatible with the latest published versions of those packages. + +Background: We encountered an issue where microsoft-agents-a365-tooling had a constraint +`microsoft-agents-hosting-core >= 0.4.0, < 0.6.0` but the samples used 0.7.0, causing +the package resolver to silently pick older versions instead of the latest. +""" + +import re +import tomllib +from pathlib import Path + +import pytest + +# Packages in this repo that should be checked for version constraint issues +INTERNAL_PACKAGES = { + "microsoft-agents-a365-tooling", + "microsoft-agents-a365-tooling-extensions-openai", + "microsoft-agents-a365-tooling-extensions-agentframework", + "microsoft-agents-a365-tooling-extensions-semantickernel", + "microsoft-agents-a365-tooling-extensions-azureaifoundry", + "microsoft-agents-a365-observability-core", + "microsoft-agents-a365-observability-extensions-openai", + "microsoft-agents-a365-observability-extensions-agent-framework", + "microsoft-agents-a365-observability-extensions-semantickernel", + "microsoft-agents-a365-runtime", + "microsoft-agents-a365-notifications", +} + +# Known external packages where we should be careful about upper bounds +EXTERNAL_PACKAGES_TO_CHECK = { + "microsoft-agents-hosting-core", + "microsoft-agents-hosting-aiohttp", + "microsoft-agents-authentication-msal", + "microsoft-agents-activity", +} + + +def get_repo_root() -> Path: + """Get the root directory of the Agent365-python repository.""" + current = Path(__file__).resolve() + # Navigate up to find the repo root (contains 'libraries' folder) + for parent in current.parents: + if (parent / "libraries").is_dir(): + return parent + raise RuntimeError("Could not find repository root") + + +def find_all_pyproject_files() -> list[Path]: + """Find all pyproject.toml files in the libraries directory.""" + repo_root = get_repo_root() + libraries_dir = repo_root / "libraries" + return list(libraries_dir.glob("**/pyproject.toml")) + + +def parse_version_constraint(constraint: str) -> dict: + """ + Parse a version constraint string and extract bounds. + + Examples: + ">= 0.4.0, < 0.6.0" -> {"lower": "0.4.0", "upper": "0.6.0", "upper_inclusive": False} + ">= 0.4.0" -> {"lower": "0.4.0", "upper": None} + + Note: Supports version numbers with any number of parts (e.g., "1.0", "1.0.0", "1.0.0.0"). + """ + result = {"lower": None, "upper": None, "upper_inclusive": False, "raw": constraint} + + # Match upper bound patterns: < X.Y or <= X.Y.Z (any dotted numeric version) + upper_match = re.search(r"(<\s*=?)\s*(\d+(?:\.\d+)*)", constraint) + if upper_match: + result["upper"] = upper_match.group(2) + result["upper_inclusive"] = upper_match.group(1).replace(" ", "") == "<=" + + # Match lower bound patterns: >= X.Y or > X.Y.Z (any dotted numeric version) + lower_match = re.search(r">=?\s*(\d+(?:\.\d+)*)", constraint) + if lower_match: + result["lower"] = lower_match.group(1) + + return result + + +def version_tuple(version: str) -> tuple: + """Convert version string to tuple for comparison.""" + # Handle pre-release versions like "0.2.1.dev2" + base_version = version.split(".dev")[0].split("a")[0].split("b")[0].split("rc")[0] + parts = base_version.split(".") + return tuple(int(p) for p in parts) + + +def is_version_compatible(version: str, upper_bound: str, inclusive: bool = False) -> bool: + """Check if a version is compatible with an upper bound constraint.""" + version_t = version_tuple(version) + upper_t = version_tuple(upper_bound) + + if inclusive: + return version_t <= upper_t + return version_t < upper_t + + +def get_dependencies_with_upper_bounds(pyproject_path: Path) -> list[dict]: + """ + Extract dependencies that have upper bound constraints. + + Returns a list of dicts with: + - package: package name + - constraint: parsed constraint info + - file: path to pyproject.toml + """ + with open(pyproject_path, "rb") as f: + data = tomllib.load(f) + + dependencies = data.get("project", {}).get("dependencies", []) + results = [] + + for dep in dependencies: + # Parse dependency string: "package-name >= 1.0.0, < 2.0.0" + match = re.match(r"^([\w\-]+)\s*(.*)$", dep.strip()) + if not match: + continue + + package_name = match.group(1) + constraint_str = match.group(2).strip() + + if not constraint_str: + continue + + constraint = parse_version_constraint(constraint_str) + + if constraint["upper"]: + results.append({ + "package": package_name, + "constraint": constraint, + "file": pyproject_path, + }) + + return results + + +class TestDependencyConstraints: + """Tests for dependency version constraints.""" + + def test_no_restrictive_upper_bounds_on_external_packages(self): + """ + Ensure we don't have overly restrictive upper bounds on external packages. + + Upper bounds like `< 0.6.0` can cause issues when the external package + releases a newer version (e.g., 0.7.0) that our samples depend on. + This causes the resolver to silently pick older versions of our packages. + """ + pyproject_files = find_all_pyproject_files() + issues = [] + + for pyproject_path in pyproject_files: + deps_with_upper = get_dependencies_with_upper_bounds(pyproject_path) + + for dep in deps_with_upper: + package = dep["package"] + + # Check if this is an external package we should monitor + if package in EXTERNAL_PACKAGES_TO_CHECK: + constraint = dep["constraint"] + relative_path = pyproject_path.relative_to(get_repo_root()) + + issues.append( + f" - {relative_path}: '{package}' has upper bound constraint " + f"'{constraint['raw']}'. This may cause resolver issues when " + f"newer versions are released." + ) + + if issues: + pytest.fail( + "Found dependencies with upper bound constraints that may cause issues:\n" + + "\n".join(issues) + + "\n\nConsider removing upper bounds or using a more permissive constraint. " + "Upper bounds on external packages can cause our packages to be downgraded " + "when newer versions of the external package are released." + ) + + def test_internal_package_constraints_are_flexible(self): + """ + Ensure internal packages don't have restrictive upper bounds on each other. + + We want internal packages to be able to evolve together without + version constraint conflicts. + """ + pyproject_files = find_all_pyproject_files() + issues = [] + + for pyproject_path in pyproject_files: + deps_with_upper = get_dependencies_with_upper_bounds(pyproject_path) + + for dep in deps_with_upper: + package = dep["package"] + + # Check if this is an internal package + if package in INTERNAL_PACKAGES: + constraint = dep["constraint"] + relative_path = pyproject_path.relative_to(get_repo_root()) + + issues.append( + f" - {relative_path}: '{package}' has upper bound constraint " + f"'{constraint['raw']}'. Internal packages should not have " + "upper bounds on each other." + ) + + if issues: + pytest.fail( + "Found internal packages with upper bound constraints:\n" + + "\n".join(issues) + + "\n\nInternal packages should use '>= X.Y.Z' without upper bounds " + "to allow them to evolve together." + ) + + def test_parse_version_constraint(self): + """Test the version constraint parser.""" + # Test with upper and lower bounds + result = parse_version_constraint(">= 0.4.0, < 0.6.0") + assert result["lower"] == "0.4.0" + assert result["upper"] == "0.6.0" + assert result["upper_inclusive"] is False + + # Test with only lower bound + result = parse_version_constraint(">= 1.0.0") + assert result["lower"] == "1.0.0" + assert result["upper"] is None + + # Test with inclusive upper bound + result = parse_version_constraint(">= 2.0.0, <= 3.0.0") + assert result["lower"] == "2.0.0" + assert result["upper"] == "3.0.0" + assert result["upper_inclusive"] is True + + def test_version_compatibility_check(self): + """Test version compatibility checking.""" + # 0.7.0 is NOT compatible with < 0.6.0 + assert is_version_compatible("0.7.0", "0.6.0", inclusive=False) is False + + # 0.5.9 IS compatible with < 0.6.0 + assert is_version_compatible("0.5.9", "0.6.0", inclusive=False) is True + + # 0.6.0 IS compatible with <= 0.6.0 + assert is_version_compatible("0.6.0", "0.6.0", inclusive=True) is True + + # 0.6.0 is NOT compatible with < 0.6.0 + assert is_version_compatible("0.6.0", "0.6.0", inclusive=False) is False diff --git a/uv.lock b/uv.lock index 27e0fd5e..029bd99c 100644 --- a/uv.lock +++ b/uv.lock @@ -1795,7 +1795,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "microsoft-agents-a365-observability-core", editable = "libraries/microsoft-agents-a365-observability-core" }, - { name = "microsoft-agents-hosting-core", specifier = ">=0.4.0,<0.6.0" }, + { name = "microsoft-agents-hosting-core", specifier = ">=0.4.0" }, { name = "opentelemetry-api", specifier = ">=1.36.0" }, ] @@ -1853,7 +1853,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, - { name = "microsoft-agents-hosting-core", specifier = ">=0.4.0,<0.6.0" }, + { name = "microsoft-agents-hosting-core", specifier = ">=0.7.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, @@ -1893,7 +1893,7 @@ requires-dist = [ { name = "azure-identity", specifier = ">=1.12.0" }, { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, { name = "microsoft-agents-a365-tooling", editable = "libraries/microsoft-agents-a365-tooling" }, - { name = "microsoft-agents-hosting-core", specifier = ">=0.4.0,<0.6.0" }, + { name = "microsoft-agents-hosting-core", specifier = ">=0.4.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=7.0.0" }, @@ -2019,19 +2019,19 @@ provides-extras = ["dev", "test"] [[package]] name = "microsoft-agents-activity" -version = "0.5.1" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/96/4416c5b3f13309d7503f3db3c2bfc321824366b68a240ed71e8145634c3d/microsoft_agents_activity-0.5.1.tar.gz", hash = "sha256:07be29aca58ea9d8279155cfa4c00261e3a18bdf718c8164c1d87e3e57ad527b", size = 55830, upload-time = "2025-10-28T19:27:03.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/e9/7f8086719f28815baca72b2f2600ce1e62b7cd53826bb406c52f28e81998/microsoft_agents_activity-0.7.0.tar.gz", hash = "sha256:77eeb6ffa9ee9e6237e1dbf5e962ea641ff60f20b0966e68e903ffbc10ebd41d", size = 60673, upload-time = "2026-01-21T18:05:24.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/47/333591538c134b5b4637ffc8ab4f5d0bf1c1b6310e3cfb5adc4002aa5940/microsoft_agents_activity-0.5.1-py3-none-any.whl", hash = "sha256:07562064125f2bc8066c2c8e9a60ff6f038f7413ccd01a9d9b0aa426e47467cd", size = 127817, upload-time = "2025-10-28T19:27:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f3/64dc3bf13e46c6a09cc1983f66da2e42bf726586fe0f77f915977a6be7d8/microsoft_agents_activity-0.7.0-py3-none-any.whl", hash = "sha256:8d30a25dfd0f491b834be52b4a21ff90ab3b9360ec7e50770c050f1d4a39e5ce", size = 132592, upload-time = "2026-01-21T18:05:33.533Z" }, ] [[package]] name = "microsoft-agents-hosting-core" -version = "0.5.1" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -2040,9 +2040,9 @@ dependencies = [ { name = "pyjwt" }, { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/0b/71bc8f2fd673de9f8a0d7e9bef30dd15892d8539c4557129a5aead2c5882/microsoft_agents_hosting_core-0.5.1.tar.gz", hash = "sha256:d9b64095bf7624d4fc9f1d48cea5a3c66cc2dee9e1c3fb6ea3e9b6dfc03ace8f", size = 81277, upload-time = "2025-10-28T19:27:08.623Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/da/26d461cb222ab41f38a3c72ca43900a65b8e8b6b71d6d1207fad1edc3e7b/microsoft_agents_hosting_core-0.7.0.tar.gz", hash = "sha256:31448279c47e39d63edc347c1d3b4de8043aa1b4c51a1f01d40d7d451221b202", size = 90446, upload-time = "2026-01-21T18:05:29.28Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/2c/bcb8d66ebfe59cf6093c5eac1fc19a7797b5b80ce3ceaec07f2954a21493/microsoft_agents_hosting_core-0.5.1-py3-none-any.whl", hash = "sha256:10a1f394d8e444f6e2e74ab935f5c0a04ebfa43d136be4658fbaccab1321c37e", size = 120190, upload-time = "2025-10-28T19:27:16.263Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e4/8d9e2e3f3a3106d0c80141631385206a6946f0b414cf863db851b98533e7/microsoft_agents_hosting_core-0.7.0-py3-none-any.whl", hash = "sha256:d03549fff01f38c1a96da4f79375c33378205ee9b5c6e01b87ba576f59b7887f", size = 133749, upload-time = "2026-01-21T18:05:38.002Z" }, ] [[package]]