From 25e7bca829bd92d7ce3cc3509e4e755f8039451d Mon Sep 17 00:00:00 2001 From: Marc Goodner Date: Mon, 1 Dec 2025 15:34:45 -0800 Subject: [PATCH 1/2] fix: support local file sources in provider discovery and config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues fixed: 1. _discover_providers_from_sources() only handled git URLs, causing local file source overrides (file://, ./, ../, /) to silently fail during provider discovery. This meant providers with local sources wouldn't appear in `amplifier init` selection. 2. list_providers() only used source discovery as a fallback when no entry points were found. Local source providers were invisible when other providers had entry points registered. 3. use_provider() used DEFAULT_PROVIDER_SOURCES instead of effective sources, causing local file overrides to be replaced with git URLs when saving provider configuration. Changes: - Rename _is_local_path to is_local_path and export it - Add FileSource handling in _discover_providers_from_sources() - Make list_providers() merge entry points with source discovery - Make use_provider() use get_effective_provider_sources() 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com> --- amplifier_app_cli/provider_manager.py | 49 ++++++++++++++++++--------- amplifier_app_cli/provider_sources.py | 6 ++-- uv.lock | 39 ++++++++------------- 3 files changed, 51 insertions(+), 43 deletions(-) diff --git a/amplifier_app_cli/provider_manager.py b/amplifier_app_cli/provider_manager.py index a9c2f3c..51f173c 100644 --- a/amplifier_app_cli/provider_manager.py +++ b/amplifier_app_cli/provider_manager.py @@ -9,7 +9,6 @@ from .lib.app_settings import AppSettings from .lib.app_settings import ScopeType from .provider_loader import get_provider_info -from .provider_sources import DEFAULT_PROVIDER_SOURCES from .provider_sources import get_effective_provider_sources logger = logging.getLogger(__name__) @@ -67,14 +66,18 @@ def use_provider( provider_id: Provider module ID (provider-anthropic, provider-openai, etc.) scope: Where to save configuration (local/project/global) config: Provider-specific configuration (model, api_key, etc.) - source: Module source URL (optional, will use canonical if not provided) + source: Module source URL (optional, will use effective source if not provided) Returns: ConfigureResult with what changed and where """ - # Determine provider source (explicit or canonical) - canonical_source = DEFAULT_PROVIDER_SOURCES.get(provider_id) - provider_source = source or canonical_source + # Determine provider source (explicit, user override, or canonical default) + if source: + provider_source = source + else: + # Check effective sources first (includes user overrides from settings) + effective_sources = get_effective_provider_sources(self.config) + provider_source = effective_sources.get(provider_id) # Build provider config entry with high priority (lower = higher priority) # Priority 1 ensures explicitly configured provider wins over profile defaults (100) @@ -155,7 +158,10 @@ def list_providers(self) -> list[tuple[str, str, str]]: Discovers providers from: 1. Installed modules (entry points) - 2. Known provider sources (resolved via GitSource, uses cache) + 2. Known provider sources (resolved via GitSource/FileSource) + + Sources are merged with entry points, allowing local overrides + to appear alongside installed providers. Returns: List of (module_id, display_name, description) tuples @@ -181,23 +187,27 @@ def list_providers(self) -> list[tuple[str, str, str]]: display_name = module.name providers[module.id] = (module.id, display_name, module.description) - # If no providers found via entry points, resolve from known sources - # This handles the case where modules were downloaded with `--target` flag - # (uv pip install --target) which doesn't register entry points - if not providers: - providers = self._discover_providers_from_sources() + # Also discover from effective sources (includes local overrides) + # This ensures providers with local file sources appear even when + # other providers were found via entry points + source_providers = self._discover_providers_from_sources() + for module_id, provider_info in source_providers.items(): + if module_id not in providers: + # Add providers not found via entry points (e.g., local overrides) + providers[module_id] = provider_info return list(providers.values()) def _discover_providers_from_sources(self) -> dict[str, tuple[str, str, str]]: """Discover providers by resolving effective sources. - Uses GitSource.resolve() to get cached module paths (same mechanism - as runtime module loading), then imports modules directly. + Uses GitSource.resolve() or FileSource.resolve() to get module paths + (same mechanism as runtime module loading), then imports modules directly. Effective sources include: 1. DEFAULT_PROVIDER_SOURCES (known providers) 2. User-added provider modules from settings + 3. User-configured source overrides (local file paths or git URLs) Returns: Dict mapping module_id to (module_id, display_name, description) tuples @@ -205,17 +215,24 @@ def _discover_providers_from_sources(self) -> dict[str, tuple[str, str, str]]: import importlib import sys + from amplifier_module_resolution.sources import FileSource from amplifier_module_resolution.sources import GitSource + from .provider_sources import is_local_path + providers: dict[str, tuple[str, str, str]] = {} # Use effective sources (includes both default and user-added providers) effective_sources = get_effective_provider_sources(self.config) for module_id, source_uri in effective_sources.items(): try: - # Resolve source to cached path (uses same caching as runtime) - git_source = GitSource.from_uri(source_uri) - module_path = git_source.resolve() + # Resolve source to path - handle both local files and git URLs + if is_local_path(source_uri): + file_source = FileSource(source_uri) + module_path = file_source.resolve() + else: + git_source = GitSource.from_uri(source_uri) + module_path = git_source.resolve() # Add to sys.path if not already there path_str = str(module_path) diff --git a/amplifier_app_cli/provider_sources.py b/amplifier_app_cli/provider_sources.py index 6648aba..16fee27 100644 --- a/amplifier_app_cli/provider_sources.py +++ b/amplifier_app_cli/provider_sources.py @@ -65,7 +65,7 @@ def get_effective_provider_sources(config_manager: "ConfigManager | None" = None return sources -def _is_local_path(source_uri: str) -> bool: +def is_local_path(source_uri: str) -> bool: """Check if source URI is a local file path. Args: @@ -121,7 +121,7 @@ def install_known_providers( console.print(f" Installing {module_id}...", end="") # Check if local file path or git URL - if _is_local_path(source_uri): + if is_local_path(source_uri): # Local file source - just validate it exists file_source = FileSource(source_uri) file_source.resolve() @@ -149,4 +149,4 @@ def install_known_providers( return installed -__all__ = ["DEFAULT_PROVIDER_SOURCES", "get_effective_provider_sources", "install_known_providers"] +__all__ = ["DEFAULT_PROVIDER_SOURCES", "get_effective_provider_sources", "install_known_providers", "is_local_path"] diff --git a/uv.lock b/uv.lock index 1a74367..77df849 100644 --- a/uv.lock +++ b/uv.lock @@ -50,7 +50,7 @@ dev = [ [[package]] name = "amplifier-collections" version = "0.1.0" -source = { git = "https://github.com/microsoft/amplifier-collections?branch=main#e9e172466181fb4db90559071b62972c275ef575" } +source = { git = "https://github.com/microsoft/amplifier-collections?branch=main#9cda7923de1b3da0ab4e2255339f8bbc09b2e6cc" } dependencies = [ { name = "pydantic" }, ] @@ -58,7 +58,7 @@ dependencies = [ [[package]] name = "amplifier-config" version = "0.1.0" -source = { git = "https://github.com/microsoft/amplifier-config?branch=main#22ecf0c96ca04597721e70eeb5b9ce19bce1fa7a" } +source = { git = "https://github.com/microsoft/amplifier-config?branch=main#568e68e8a6a5e5f8036bc5aaa5360542f9bbf3dc" } dependencies = [ { name = "pyyaml" }, ] @@ -66,8 +66,9 @@ dependencies = [ [[package]] name = "amplifier-core" version = "1.0.0" -source = { git = "https://github.com/microsoft/amplifier-core?branch=main#351fa85877b2c62987190694ab760e9bf3063f1f" } +source = { git = "https://github.com/microsoft/amplifier-core?branch=main#26082ca0d5f09b5130076c18c1b70311428e7b5e" } dependencies = [ + { name = "click" }, { name = "pydantic" }, { name = "pyyaml" }, { name = "tomli" }, @@ -77,12 +78,12 @@ dependencies = [ [[package]] name = "amplifier-module-resolution" version = "0.1.0" -source = { git = "https://github.com/microsoft/amplifier-module-resolution?branch=main#6a956cf541e31e692edc979d629a0d184c41d15e" } +source = { git = "https://github.com/microsoft/amplifier-module-resolution?branch=main#a44af3d573f251fe100955d5d5223d83cb07ee04" } [[package]] name = "amplifier-profiles" version = "0.1.0" -source = { git = "https://github.com/microsoft/amplifier-profiles?branch=main#c7e40ef95510dd01718eba881874b8f795466dc8" } +source = { git = "https://github.com/microsoft/amplifier-profiles?branch=main#ff4f864a2eabd41a7f970718cd799b382083a2dd" } dependencies = [ { name = "amplifier-collections" }, { name = "pydantic" }, @@ -99,16 +100,15 @@ wheels = [ [[package]] name = "anyio" -version = "4.11.0" +version = "4.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, ] [[package]] @@ -122,14 +122,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.0" +version = "8.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[package]] @@ -254,7 +254,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.4" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -262,9 +262,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] @@ -470,15 +470,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, ] -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - [[package]] name = "socksio" version = "1.0.0" From 409538ae9477bf7d3c0d596616e48152241f25d8 Mon Sep 17 00:00:00 2001 From: Marc Goodner Date: Tue, 2 Dec 2025 09:15:12 -0800 Subject: [PATCH 2/2] fix: add None check for amplifier_core.__file__ before accessing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevents AttributeError when __file__ is None in certain import contexts. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com> --- amplifier_app_cli/commands/module.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/amplifier_app_cli/commands/module.py b/amplifier_app_cli/commands/module.py index d54cfa2..ba9edbe 100644 --- a/amplifier_app_cli/commands/module.py +++ b/amplifier_app_cli/commands/module.py @@ -587,6 +587,9 @@ def _run_behavioral_tests(module_path: str, module_type: str) -> bool: try: import amplifier_core + if amplifier_core.__file__ is None: + console.print("[red]amplifier-core __file__ not available - cannot locate behavioral tests[/red]") + return False core_path = Path(amplifier_core.__file__).parent test_file = core_path / "validation" / "behavioral" / f"test_{module_type}.py"