diff --git a/.github/actions/setup-test-environment/action.yml b/.github/actions/setup-test-environment/action.yml index 478e8f598..ee9011ed8 100644 --- a/.github/actions/setup-test-environment/action.yml +++ b/.github/actions/setup-test-environment/action.yml @@ -57,7 +57,7 @@ runs: echo "Building Llama Stack" LLAMA_STACK_DIR=. \ - uv run --no-sync llama stack build --template ci-tests --image-type venv + uv run --no-sync llama stack list-deps ci-tests | xargs -L1 uv pip install - name: Configure git for commits shell: bash diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 29acdce59..00a8f54ac 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -14,6 +14,7 @@ Llama Stack uses GitHub Actions for Continuous Integration (CI). Below is a tabl | Pre-commit | [pre-commit.yml](pre-commit.yml) | Run pre-commit checks | | Pre-commit Bot | [precommit-trigger.yml](precommit-trigger.yml) | Pre-commit bot for PR | | Test Llama Stack Build | [providers-build.yml](providers-build.yml) | Test llama stack build | +| Test llama stack list-deps | [providers-list-deps.yml](providers-list-deps.yml) | Test llama stack list-deps | | Python Package Build Test | [python-build-test.yml](python-build-test.yml) | Test building the llama-stack PyPI project | | Integration Tests (Record) | [record-integration-tests.yml](record-integration-tests.yml) | Run the integration test suite from tests/integration | | Check semantic PR titles | [semantic-pr.yml](semantic-pr.yml) | Ensure that PR titles follow the conventional commit spec | diff --git a/.github/workflows/integration-vector-io-tests.yml b/.github/workflows/integration-vector-io-tests.yml index fe5785c73..e9a758873 100644 --- a/.github/workflows/integration-vector-io-tests.yml +++ b/.github/workflows/integration-vector-io-tests.yml @@ -144,7 +144,7 @@ jobs: - name: Build Llama Stack run: | - uv run --no-sync llama stack build --template ci-tests --image-type venv + uv run --no-sync llama stack list-deps ci-tests | xargs -L1 uv pip install - name: Check Storage and Memory Available Before Tests if: ${{ always() }} diff --git a/.github/workflows/providers-list-deps.yml b/.github/workflows/providers-list-deps.yml new file mode 100644 index 000000000..df491b680 --- /dev/null +++ b/.github/workflows/providers-list-deps.yml @@ -0,0 +1,105 @@ +name: Test llama stack list-deps + +run-name: Test llama stack list-deps + +on: + push: + branches: + - main + paths: + - 'llama_stack/cli/stack/list_deps.py' + - 'llama_stack/cli/stack/_list_deps.py' + - 'llama_stack/core/build.*' + - 'llama_stack/core/*.sh' + - '.github/workflows/providers-list-deps.yml' + - 'llama_stack/templates/**' + - 'pyproject.toml' + + pull_request: + paths: + - 'llama_stack/cli/stack/list_deps.py' + - 'llama_stack/cli/stack/_list_deps.py' + - 'llama_stack/core/build.*' + - 'llama_stack/core/*.sh' + - '.github/workflows/providers-list-deps.yml' + - 'llama_stack/templates/**' + - 'pyproject.toml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + generate-matrix: + runs-on: ubuntu-latest + outputs: + distros: ${{ steps.set-matrix.outputs.distros }} + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Generate Distribution List + id: set-matrix + run: | + distros=$(ls llama_stack/distributions/*/*build.yaml | awk -F'/' '{print $(NF-1)}' | jq -R -s -c 'split("\n")[:-1]') + echo "distros=$distros" >> "$GITHUB_OUTPUT" + + list-deps: + needs: generate-matrix + runs-on: ubuntu-latest + strategy: + matrix: + distro: ${{ fromJson(needs.generate-matrix.outputs.distros) }} + image-type: [venv, container] + fail-fast: false # We want to run all jobs even if some fail + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install dependencies + uses: ./.github/actions/setup-runner + + - name: Print dependencies + run: | + uv run llama stack list-deps ${{ matrix.distro }} + + - name: Install Distro using llama stack list-deps + run: | + # USE_COPY_NOT_MOUNT is set to true since mounting is not supported by docker buildx, we use COPY instead + # LLAMA_STACK_DIR is set to the current directory so we are building from the source + USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run llama stack list-deps ${{ matrix.distro }} | xargs -L1 uv pip install + + - name: Print dependencies in the image + if: matrix.image-type == 'venv' + run: | + uv pip list + + show-single-provider: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install dependencies + uses: ./.github/actions/setup-runner + + - name: Show a single provider + run: | + USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run llama stack list-deps --providers inference=remote::ollama + + list-deps-from-config: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install dependencies + uses: ./.github/actions/setup-runner + + - name: list-des from Config + env: + USE_COPY_NOT_MOUNT: "true" + LLAMA_STACK_DIR: "." + run: | + uv run llama stack list-deps llama_stack/distributions/ci-tests/build.yaml diff --git a/.github/workflows/test-external.yml b/.github/workflows/test-external.yml index a008b17af..a1013ad9e 100644 --- a/.github/workflows/test-external.yml +++ b/.github/workflows/test-external.yml @@ -44,11 +44,14 @@ jobs: - name: Print distro dependencies run: | - USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run --no-sync llama stack build --config tests/external/build.yaml --print-deps-only + USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run --no-sync llama stack list-deps tests/external/build.yaml - name: Build distro from config file run: | - USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run --no-sync llama stack build --config tests/external/build.yaml + uv venv ci-test + source ci-test/bin/activate + uv pip install -e . + LLAMA_STACK_LOGGING=all=CRITICAL llama stack list-deps tests/external/build.yaml | xargs -L1 uv pip install - name: Start Llama Stack server in background if: ${{ matrix.image-type }} == 'venv' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eab182eea..c869b4f5c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -167,9 +167,9 @@ under the LICENSE file in the root directory of this source tree. Some tips about common tasks you work on while contributing to Llama Stack: -### Using `llama stack build` +### Installing dependencies of distributions -Building a stack image will use the production version of the `llama-stack` and `llama-stack-client` packages. If you are developing with a llama-stack repository checked out and need your code to be reflected in the stack image, set `LLAMA_STACK_DIR` and `LLAMA_STACK_CLIENT_DIR` to the appropriate checked out directories when running any of the `llama` CLI commands. +When installing dependencies for a distribution, you can use `llama stack list-deps` to view and install the required packages. Example: ```bash @@ -177,7 +177,12 @@ cd work/ git clone https://github.com/llamastack/llama-stack.git git clone https://github.com/llamastack/llama-stack-client-python.git cd llama-stack -LLAMA_STACK_DIR=$(pwd) LLAMA_STACK_CLIENT_DIR=../llama-stack-client-python llama stack build --distro <...> + +# Show dependencies for a distribution +llama stack list-deps + +# Install dependencies +llama stack list-deps | xargs -L1 uv pip install ``` ### Updating distribution configurations diff --git a/README.md b/README.md index 75e9989d7..4122440af 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,11 @@ MODEL="Llama-4-Scout-17B-16E-Instruct" # get meta url from llama.com huggingface-cli download meta-llama/$MODEL --local-dir ~/.llama/$MODEL +# install dependencies for the distribution +llama stack list-deps meta-reference-gpu | xargs -L1 uv pip install + # start a llama stack server -INFERENCE_MODEL=meta-llama/$MODEL llama stack build --run --template meta-reference-gpu +INFERENCE_MODEL=meta-llama/$MODEL llama stack run meta-reference-gpu # install client to interact with the server pip install llama-stack-client diff --git a/docs/docs/contributing/index.mdx b/docs/docs/contributing/index.mdx index 263900ecc..2051f6040 100644 --- a/docs/docs/contributing/index.mdx +++ b/docs/docs/contributing/index.mdx @@ -158,7 +158,7 @@ under the LICENSE file in the root directory of this source tree. Some tips about common tasks you work on while contributing to Llama Stack: -### Using `llama stack build` +### Installing dependencies of distributions Building a stack image will use the production version of the `llama-stack` and `llama-stack-client` packages. If you are developing with a llama-stack repository checked out and need your code to be reflected in the stack image, set `LLAMA_STACK_DIR` and `LLAMA_STACK_CLIENT_DIR` to the appropriate checked out directories when running any of the `llama` CLI commands. @@ -168,7 +168,7 @@ cd work/ git clone https://github.com/meta-llama/llama-stack.git git clone https://github.com/meta-llama/llama-stack-client-python.git cd llama-stack -LLAMA_STACK_DIR=$(pwd) LLAMA_STACK_CLIENT_DIR=../llama-stack-client-python llama stack build --distro <...> +llama stack build --distro <...> ``` ### Updating distribution configurations diff --git a/docs/docs/distributions/self_hosted_distro/starter.md b/docs/docs/distributions/self_hosted_distro/starter.md index a8faf713a..e04c5874b 100644 --- a/docs/docs/distributions/self_hosted_distro/starter.md +++ b/docs/docs/distributions/self_hosted_distro/starter.md @@ -169,7 +169,11 @@ docker run \ Ensure you have configured the starter distribution using the environment variables explained above. ```bash -uv run --with llama-stack llama stack build --distro starter --image-type venv --run +# Install dependencies for the starter distribution +uv run --with llama-stack llama stack list-deps starter | xargs -L1 uv pip install + +# Run the server +uv run --with llama-stack llama stack run starter ``` ## Example Usage diff --git a/docs/docs/getting_started/detailed_tutorial.mdx b/docs/docs/getting_started/detailed_tutorial.mdx index 45373e2ab..888ea2414 100644 --- a/docs/docs/getting_started/detailed_tutorial.mdx +++ b/docs/docs/getting_started/detailed_tutorial.mdx @@ -58,15 +58,19 @@ Llama Stack is a server that exposes multiple APIs, you connect with it using th -You can use Python to build and run the Llama Stack server, which is useful for testing and development. +You can use Python to install dependencies and run the Llama Stack server, which is useful for testing and development. Llama Stack uses a [YAML configuration file](../distributions/configuration) to specify the stack setup, which defines the providers and their settings. The generated configuration serves as a starting point that you can [customize for your specific needs](../distributions/customizing_run_yaml). -Now let's build and run the Llama Stack config for Ollama. +Now let's install dependencies and run the Llama Stack config for Ollama. We use `starter` as template. By default all providers are disabled, this requires enable ollama by passing environment variables. ```bash -llama stack build --distro starter --image-type venv --run +# Install dependencies for the starter distribution +uv run --with llama-stack llama stack list-deps starter | xargs -L1 uv pip install + +# Run the server +llama stack run starter ``` diff --git a/docs/docs/getting_started/quickstart.mdx b/docs/docs/getting_started/quickstart.mdx index b885f3c66..2e47a771e 100644 --- a/docs/docs/getting_started/quickstart.mdx +++ b/docs/docs/getting_started/quickstart.mdx @@ -24,10 +24,13 @@ ollama run llama3.2:3b --keepalive 60m #### Step 2: Run the Llama Stack server -We will use `uv` to run the Llama Stack server. +We will use `uv` to install dependencies and run the Llama Stack server. ```bash -OLLAMA_URL=http://localhost:11434 \ - uv run --with llama-stack llama stack build --distro starter --image-type venv --run +# Install dependencies for the starter distribution +uv run --with llama-stack llama stack list-deps starter | xargs -L1 uv pip install + +# Run the server +OLLAMA_URL=http://localhost:11434 uv run --with llama-stack llama stack run starter ``` #### Step 3: Run the demo Now open up a new terminal and copy the following script into a file named `demo_script.py`. diff --git a/llama_stack/cli/stack/_list_deps.py b/llama_stack/cli/stack/_list_deps.py new file mode 100644 index 000000000..18141be5f --- /dev/null +++ b/llama_stack/cli/stack/_list_deps.py @@ -0,0 +1,182 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the terms described in the LICENSE file in +# the root directory of this source tree. + +import argparse +import sys +from pathlib import Path + +import yaml +from termcolor import cprint + +from llama_stack.cli.stack.utils import ImageType +from llama_stack.core.build import get_provider_dependencies +from llama_stack.core.datatypes import ( + BuildConfig, + BuildProvider, + DistributionSpec, +) +from llama_stack.core.distribution import get_provider_registry +from llama_stack.core.stack import replace_env_vars +from llama_stack.log import get_logger +from llama_stack.providers.datatypes import Api + +TEMPLATES_PATH = Path(__file__).parent.parent.parent / "templates" + +logger = get_logger(name=__name__, category="cli") + + +# These are the dependencies needed by the distribution server. +# `llama-stack` is automatically installed by the installation script. +SERVER_DEPENDENCIES = [ + "aiosqlite", + "fastapi", + "fire", + "httpx", + "uvicorn", + "opentelemetry-sdk", + "opentelemetry-exporter-otlp-proto-http", +] + + +def format_output_deps_only( + normal_deps: list[str], + special_deps: list[str], + external_deps: list[str], + uv: bool = False, +) -> str: + """Format dependencies as a list.""" + lines = [] + + uv_str = "" + if uv: + uv_str = "uv pip install " + + # Quote deps with commas + quoted_normal_deps = [quote_if_needed(dep) for dep in normal_deps] + lines.append(f"{uv_str}{' '.join(quoted_normal_deps)}") + + for special_dep in special_deps: + lines.append(f"{uv_str}{quote_special_dep(special_dep)}") + + for external_dep in external_deps: + lines.append(f"{uv_str}{quote_special_dep(external_dep)}") + + return "\n".join(lines) + + +def run_stack_list_deps_command(args: argparse.Namespace) -> None: + if args.config: + try: + from llama_stack.core.utils.config_resolution import Mode, resolve_config_or_distro + + config_file = resolve_config_or_distro(args.config, Mode.BUILD) + except ValueError as e: + cprint( + f"Could not parse config file {args.config}: {e}", + color="red", + file=sys.stderr, + ) + sys.exit(1) + if config_file: + with open(config_file) as f: + try: + contents = yaml.safe_load(f) + contents = replace_env_vars(contents) + build_config = BuildConfig(**contents) + build_config.image_type = "venv" + except Exception as e: + cprint( + f"Could not parse config file {config_file}: {e}", + color="red", + file=sys.stderr, + ) + sys.exit(1) + elif args.providers: + provider_list: dict[str, list[BuildProvider]] = dict() + for api_provider in args.providers.split(","): + if "=" not in api_provider: + cprint( + "Could not parse `--providers`. Please ensure the list is in the format api1=provider1,api2=provider2", + color="red", + file=sys.stderr, + ) + sys.exit(1) + api, provider_type = api_provider.split("=") + providers_for_api = get_provider_registry().get(Api(api), None) + if providers_for_api is None: + cprint( + f"{api} is not a valid API.", + color="red", + file=sys.stderr, + ) + sys.exit(1) + if provider_type in providers_for_api: + provider = BuildProvider( + provider_type=provider_type, + module=None, + ) + provider_list.setdefault(api, []).append(provider) + else: + cprint( + f"{provider_type} is not a valid provider for the {api} API.", + color="red", + file=sys.stderr, + ) + sys.exit(1) + distribution_spec = DistributionSpec( + providers=provider_list, + description=",".join(args.providers), + ) + build_config = BuildConfig(image_type=ImageType.VENV.value, distribution_spec=distribution_spec) + + normal_deps, special_deps, external_provider_dependencies = get_provider_dependencies(build_config) + normal_deps += SERVER_DEPENDENCIES + + # Add external API dependencies + if build_config.external_apis_dir: + from llama_stack.core.external import load_external_apis + + external_apis = load_external_apis(build_config) + if external_apis: + for _, api_spec in external_apis.items(): + normal_deps.extend(api_spec.pip_packages) + + # Format and output based on requested format + output = format_output_deps_only( + normal_deps=normal_deps, + special_deps=special_deps, + external_deps=external_provider_dependencies, + uv=args.format == "uv", + ) + + print(output) + + +def quote_if_needed(dep): + # Add quotes if the dependency contains special characters that need escaping in shell + # This includes: commas, comparison operators (<, >, <=, >=, ==, !=) + needs_quoting = any(char in dep for char in [",", "<", ">", "="]) + return f"'{dep}'" if needs_quoting else dep + + +def quote_special_dep(dep_string): + """ + Quote individual packages in a special dependency string. + Special deps may contain multiple packages and flags like --extra-index-url. + We need to quote only the package specs that contain special characters. + """ + parts = dep_string.split() + quoted_parts = [] + + for part in parts: + # Don't quote flags (they start with -) + if part.startswith("-"): + quoted_parts.append(part) + else: + # Quote package specs that need it + quoted_parts.append(quote_if_needed(part)) + + return " ".join(quoted_parts) diff --git a/llama_stack/cli/stack/build.py b/llama_stack/cli/stack/build.py index 80cf6fb38..cbe8ed881 100644 --- a/llama_stack/cli/stack/build.py +++ b/llama_stack/cli/stack/build.py @@ -8,6 +8,9 @@ import textwrap from llama_stack.cli.stack.utils import ImageType from llama_stack.cli.subcommand import Subcommand +from llama_stack.log import get_logger + +logger = get_logger(__name__, category="cli") class StackBuild(Subcommand): @@ -16,7 +19,7 @@ class StackBuild(Subcommand): self.parser = subparsers.add_parser( "build", prog="llama stack build", - description="Build a Llama stack container", + description="[DEPRECATED] Build a Llama stack container. This command is deprecated and will be removed in a future release. Use `llama stack list-deps ' instead.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) self._add_arguments() @@ -93,6 +96,9 @@ the build. If not specified, currently active environment will be used if found. ) def _run_stack_build_command(self, args: argparse.Namespace) -> None: + logger.warning( + "The 'llama stack build' command is deprecated and will be removed in a future release. Please use 'llama stack list-deps'" + ) # always keep implementation completely silo-ed away from CLI so CLI # can be fast to load and reduces dependencies from ._build import run_stack_build_command diff --git a/llama_stack/cli/stack/list_deps.py b/llama_stack/cli/stack/list_deps.py new file mode 100644 index 000000000..b6eee1f3b --- /dev/null +++ b/llama_stack/cli/stack/list_deps.py @@ -0,0 +1,51 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the terms described in the LICENSE file in +# the root directory of this source tree. +import argparse + +from llama_stack.cli.subcommand import Subcommand + + +class StackListDeps(Subcommand): + def __init__(self, subparsers: argparse._SubParsersAction): + super().__init__() + self.parser = subparsers.add_parser( + "list-deps", + prog="llama stack list-deps", + description="list the dependencies for a llama stack distribution", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + self._add_arguments() + self.parser.set_defaults(func=self._run_stack_list_deps_command) + + def _add_arguments(self): + self.parser.add_argument( + "config", + type=str, + nargs="?", # Make it optional + metavar="config | distro", + help="Path to config file to use or name of known distro (llama stack list for a list).", + ) + + self.parser.add_argument( + "--providers", + type=str, + default=None, + help="sync dependencies for a list of providers and only those providers. This list is formatted like: api1=provider1,api2=provider2. Where there can be multiple providers per API.", + ) + self.parser.add_argument( + "--format", + type=str, + choices=["uv", "deps-only"], + default="deps-only", + help="Output format: 'uv' shows shell commands, 'deps-only' shows just the list of dependencies without `uv` (default)", + ) + + def _run_stack_list_deps_command(self, args: argparse.Namespace) -> None: + # always keep implementation completely silo-ed away from CLI so CLI + # can be fast to load and reduces dependencies + from ._list_deps import run_stack_list_deps_command + + return run_stack_list_deps_command(args) diff --git a/llama_stack/cli/stack/stack.py b/llama_stack/cli/stack/stack.py index 3aff78e23..fd0a4edf5 100644 --- a/llama_stack/cli/stack/stack.py +++ b/llama_stack/cli/stack/stack.py @@ -13,6 +13,7 @@ from llama_stack.cli.subcommand import Subcommand from .build import StackBuild from .list_apis import StackListApis +from .list_deps import StackListDeps from .list_providers import StackListProviders from .remove import StackRemove from .run import StackRun @@ -39,6 +40,7 @@ class StackParser(Subcommand): subparsers = self.parser.add_subparsers(title="stack_subcommands") # Add sub-commands + StackListDeps.create(subparsers) StackBuild.create(subparsers) StackListApis.create(subparsers) StackListProviders.create(subparsers) diff --git a/llama_stack/cli/stack/utils.py b/llama_stack/cli/stack/utils.py index fdf9e1761..4d4c1b538 100644 --- a/llama_stack/cli/stack/utils.py +++ b/llama_stack/cli/stack/utils.py @@ -4,7 +4,28 @@ # This source code is licensed under the terms described in the LICENSE file in # the root directory of this source tree. +import json +import sys from enum import Enum +from functools import lru_cache +from pathlib import Path + +import yaml +from termcolor import cprint + +from llama_stack.core.datatypes import ( + BuildConfig, + Provider, + StackRunConfig, +) +from llama_stack.core.distribution import get_provider_registry +from llama_stack.core.resolver import InvalidProviderError +from llama_stack.core.utils.config_dirs import EXTERNAL_PROVIDERS_DIR +from llama_stack.core.utils.dynamic import instantiate_class_type +from llama_stack.core.utils.image_types import LlamaStackImageType +from llama_stack.providers.datatypes import Api + +TEMPLATES_PATH = Path(__file__).parent.parent.parent / "distributions" class ImageType(Enum): @@ -19,3 +40,91 @@ def print_subcommand_description(parser, subparsers): description = subcommand.description description_text += f" {name:<21} {description}\n" parser.epilog = description_text + + +def generate_run_config( + build_config: BuildConfig, + build_dir: Path, + image_name: str, +) -> Path: + """ + Generate a run.yaml template file for user to edit from a build.yaml file + """ + apis = list(build_config.distribution_spec.providers.keys()) + run_config = StackRunConfig( + container_image=(image_name if build_config.image_type == LlamaStackImageType.CONTAINER.value else None), + image_name=image_name, + apis=apis, + providers={}, + external_providers_dir=build_config.external_providers_dir + if build_config.external_providers_dir + else EXTERNAL_PROVIDERS_DIR, + ) + # build providers dict + provider_registry = get_provider_registry(build_config) + for api in apis: + run_config.providers[api] = [] + providers = build_config.distribution_spec.providers[api] + + for provider in providers: + pid = provider.provider_type.split("::")[-1] + + p = provider_registry[Api(api)][provider.provider_type] + if p.deprecation_error: + raise InvalidProviderError(p.deprecation_error) + + try: + config_type = instantiate_class_type(provider_registry[Api(api)][provider.provider_type].config_class) + except (ModuleNotFoundError, ValueError) as exc: + # HACK ALERT: + # This code executes after building is done, the import cannot work since the + # package is either available in the venv or container - not available on the host. + # TODO: use a "is_external" flag in ProviderSpec to check if the provider is + # external + cprint( + f"Failed to import provider {provider.provider_type} for API {api} - assuming it's external, skipping: {exc}", + color="yellow", + file=sys.stderr, + ) + # Set config_type to None to avoid UnboundLocalError + config_type = None + + if config_type is not None and hasattr(config_type, "sample_run_config"): + config = config_type.sample_run_config(__distro_dir__=f"~/.llama/distributions/{image_name}") + else: + config = {} + + p_spec = Provider( + provider_id=pid, + provider_type=provider.provider_type, + config=config, + module=provider.module, + ) + run_config.providers[api].append(p_spec) + + run_config_file = build_dir / f"{image_name}-run.yaml" + + with open(run_config_file, "w") as f: + to_write = json.loads(run_config.model_dump_json()) + f.write(yaml.dump(to_write, sort_keys=False)) + + # Only print this message for non-container builds since it will be displayed before the + # container is built + # For non-container builds, the run.yaml is generated at the very end of the build process so it + # makes sense to display this message + if build_config.image_type != LlamaStackImageType.CONTAINER.value: + cprint(f"You can now run your stack with `llama stack run {run_config_file}`", color="green", file=sys.stderr) + return run_config_file + + +@lru_cache +def available_templates_specs() -> dict[str, BuildConfig]: + import yaml + + template_specs = {} + for p in TEMPLATES_PATH.rglob("*build.yaml"): + template_name = p.parent.name + with open(p) as f: + build_config = BuildConfig(**yaml.safe_load(f)) + template_specs[template_name] = build_config + return template_specs diff --git a/llama_stack/core/resolver.py b/llama_stack/core/resolver.py index 73c047979..acd459f99 100644 --- a/llama_stack/core/resolver.py +++ b/llama_stack/core/resolver.py @@ -4,6 +4,7 @@ # This source code is licensed under the terms described in the LICENSE file in # the root directory of this source tree. import importlib +import importlib.metadata import inspect from typing import Any diff --git a/llama_stack/core/utils/config_resolution.py b/llama_stack/core/utils/config_resolution.py index 182a571ee..fcf057db6 100644 --- a/llama_stack/core/utils/config_resolution.py +++ b/llama_stack/core/utils/config_resolution.py @@ -42,25 +42,25 @@ def resolve_config_or_distro( # Strategy 1: Try as file path first config_path = Path(config_or_distro) if config_path.exists() and config_path.is_file(): - logger.info(f"Using file path: {config_path}") + logger.debug(f"Using file path: {config_path}") return config_path.resolve() # Strategy 2: Try as distribution name (if no .yaml extension) if not config_or_distro.endswith(".yaml"): distro_config = _get_distro_config_path(config_or_distro, mode) if distro_config.exists(): - logger.info(f"Using distribution: {distro_config}") + logger.debug(f"Using distribution: {distro_config}") return distro_config # Strategy 3: Try as built distribution name distrib_config = DISTRIBS_BASE_DIR / f"llamastack-{config_or_distro}" / f"{config_or_distro}-{mode}.yaml" if distrib_config.exists(): - logger.info(f"Using built distribution: {distrib_config}") + logger.debug(f"Using built distribution: {distrib_config}") return distrib_config distrib_config = DISTRIBS_BASE_DIR / f"{config_or_distro}" / f"{config_or_distro}-{mode}.yaml" if distrib_config.exists(): - logger.info(f"Using built distribution: {distrib_config}") + logger.debug(f"Using built distribution: {distrib_config}") return distrib_config # Strategy 4: Failed - provide helpful error diff --git a/tests/unit/distribution/test_stack_list_deps.py b/tests/unit/distribution/test_stack_list_deps.py new file mode 100644 index 000000000..7725ed870 --- /dev/null +++ b/tests/unit/distribution/test_stack_list_deps.py @@ -0,0 +1,50 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the terms described in the LICENSE file in +# the root directory of this source tree. + +import argparse +from io import StringIO +from unittest.mock import patch + +from llama_stack.cli.stack._list_deps import ( + run_stack_list_deps_command, +) + + +def test_stack_list_deps_basic(): + args = argparse.Namespace( + config=None, + env_name="test-env", + providers="inference=remote::ollama", + format="deps-only", + ) + + with patch("sys.stdout", new_callable=StringIO) as mock_stdout: + run_stack_list_deps_command(args) + output = mock_stdout.getvalue() + + # deps-only format should NOT include "uv pip install" or "Dependencies for" + assert "uv pip install" not in output + assert "Dependencies for" not in output + + # Check that expected dependencies are present + assert "ollama" in output + assert "aiohttp" in output + assert "fastapi" in output + + +def test_stack_list_deps_with_distro_uv(): + args = argparse.Namespace( + config="starter", + env_name=None, + providers=None, + format="uv", + ) + + with patch("sys.stdout", new_callable=StringIO) as mock_stdout: + run_stack_list_deps_command(args) + output = mock_stdout.getvalue() + + assert "uv pip install" in output diff --git a/uv.lock b/uv.lock index 747e82aaa..f9806123d 100644 --- a/uv.lock +++ b/uv.lock @@ -4129,27 +4129,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.5" +version = "0.9.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/cd/01015eb5034605fd98d829c5839ec2c6b4582b479707f7c1c2af861e8258/ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e", size = 5170722, upload-time = "2025-07-24T13:26:37.456Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/8e/fafaa6f15c332e73425d9c44ada85360501045d5ab0b81400076aff27cf6/ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7", size = 3759776, upload-time = "2025-03-07T15:27:44.363Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/de/ad2f68f0798ff15dd8c0bcc2889558970d9a685b3249565a937cd820ad34/ruff-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:1de2c887e9dec6cb31fcb9948299de5b2db38144e66403b9660c9548a67abd92", size = 11819133, upload-time = "2025-07-24T13:25:56.369Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fc/c6b65cd0e7fbe60f17e7ad619dca796aa49fbca34bb9bea5f8faf1ec2643/ruff-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1ab65e7d8152f519e7dea4de892317c9da7a108da1c56b6a3c1d5e7cf4c5e9a", size = 12501114, upload-time = "2025-07-24T13:25:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/c5/de/c6bec1dce5ead9f9e6a946ea15e8d698c35f19edc508289d70a577921b30/ruff-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:962775ed5b27c7aa3fdc0d8f4d4433deae7659ef99ea20f783d666e77338b8cf", size = 11716873, upload-time = "2025-07-24T13:26:01.496Z" }, - { url = "https://files.pythonhosted.org/packages/a1/16/cf372d2ebe91e4eb5b82a2275c3acfa879e0566a7ac94d331ea37b765ac8/ruff-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b4cae449597e7195a49eb1cdca89fd9fbb16140c7579899e87f4c85bf82f73", size = 11958829, upload-time = "2025-07-24T13:26:03.721Z" }, - { url = "https://files.pythonhosted.org/packages/25/bf/cd07e8f6a3a6ec746c62556b4c4b79eeb9b0328b362bb8431b7b8afd3856/ruff-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b13489c3dc50de5e2d40110c0cce371e00186b880842e245186ca862bf9a1ac", size = 11626619, upload-time = "2025-07-24T13:26:06.118Z" }, - { url = "https://files.pythonhosted.org/packages/d8/c9/c2ccb3b8cbb5661ffda6925f81a13edbb786e623876141b04919d1128370/ruff-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1504fea81461cf4841778b3ef0a078757602a3b3ea4b008feb1308cb3f23e08", size = 13221894, upload-time = "2025-07-24T13:26:08.292Z" }, - { url = "https://files.pythonhosted.org/packages/6b/58/68a5be2c8e5590ecdad922b2bcd5583af19ba648f7648f95c51c3c1eca81/ruff-0.12.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7da4129016ae26c32dfcbd5b671fe652b5ab7fc40095d80dcff78175e7eddd4", size = 14163909, upload-time = "2025-07-24T13:26:10.474Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d1/ef6b19622009ba8386fdb792c0743f709cf917b0b2f1400589cbe4739a33/ruff-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca972c80f7ebcfd8af75a0f18b17c42d9f1ef203d163669150453f50ca98ab7b", size = 13583652, upload-time = "2025-07-24T13:26:13.381Z" }, - { url = "https://files.pythonhosted.org/packages/62/e3/1c98c566fe6809a0c83751d825a03727f242cdbe0d142c9e292725585521/ruff-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dbbf9f25dfb501f4237ae7501d6364b76a01341c6f1b2cd6764fe449124bb2a", size = 12700451, upload-time = "2025-07-24T13:26:15.488Z" }, - { url = "https://files.pythonhosted.org/packages/24/ff/96058f6506aac0fbc0d0fc0d60b0d0bd746240a0594657a2d94ad28033ba/ruff-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c47dea6ae39421851685141ba9734767f960113d51e83fd7bb9958d5be8763a", size = 12937465, upload-time = "2025-07-24T13:26:17.808Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d3/68bc5e7ab96c94b3589d1789f2dd6dd4b27b263310019529ac9be1e8f31b/ruff-0.12.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5076aa0e61e30f848846f0265c873c249d4b558105b221be1828f9f79903dc5", size = 11771136, upload-time = "2025-07-24T13:26:20.422Z" }, - { url = "https://files.pythonhosted.org/packages/52/75/7356af30a14584981cabfefcf6106dea98cec9a7af4acb5daaf4b114845f/ruff-0.12.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5a4c7830dadd3d8c39b1cc85386e2c1e62344f20766be6f173c22fb5f72f293", size = 11601644, upload-time = "2025-07-24T13:26:22.928Z" }, - { url = "https://files.pythonhosted.org/packages/c2/67/91c71d27205871737cae11025ee2b098f512104e26ffd8656fd93d0ada0a/ruff-0.12.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:46699f73c2b5b137b9dc0fc1a190b43e35b008b398c6066ea1350cce6326adcb", size = 12478068, upload-time = "2025-07-24T13:26:26.134Z" }, - { url = "https://files.pythonhosted.org/packages/34/04/b6b00383cf2f48e8e78e14eb258942fdf2a9bf0287fbf5cdd398b749193a/ruff-0.12.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a655a0a0d396f0f072faafc18ebd59adde8ca85fb848dc1b0d9f024b9c4d3bb", size = 12991537, upload-time = "2025-07-24T13:26:28.533Z" }, - { url = "https://files.pythonhosted.org/packages/3e/b9/053d6445dc7544fb6594785056d8ece61daae7214859ada4a152ad56b6e0/ruff-0.12.5-py3-none-win32.whl", hash = "sha256:dfeb2627c459b0b78ca2bbdc38dd11cc9a0a88bf91db982058b26ce41714ffa9", size = 11751575, upload-time = "2025-07-24T13:26:30.835Z" }, - { url = "https://files.pythonhosted.org/packages/bc/0f/ab16e8259493137598b9149734fec2e06fdeda9837e6f634f5c4e35916da/ruff-0.12.5-py3-none-win_amd64.whl", hash = "sha256:ae0d90cf5f49466c954991b9d8b953bd093c32c27608e409ae3564c63c5306a5", size = 12882273, upload-time = "2025-07-24T13:26:32.929Z" }, - { url = "https://files.pythonhosted.org/packages/00/db/c376b0661c24cf770cb8815268190668ec1330eba8374a126ceef8c72d55/ruff-0.12.5-py3-none-win_arm64.whl", hash = "sha256:48cdbfc633de2c5c37d9f090ba3b352d1576b0015bfc3bc98eaf230275b7e805", size = 11951564, upload-time = "2025-07-24T13:26:34.994Z" }, + { url = "https://files.pythonhosted.org/packages/73/b2/af7c2cc9e438cbc19fafeec4f20bfcd72165460fe75b2b6e9a0958c8c62b/ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d", size = 10049494, upload-time = "2025-03-07T15:26:51.268Z" }, + { url = "https://files.pythonhosted.org/packages/6d/12/03f6dfa1b95ddd47e6969f0225d60d9d7437c91938a310835feb27927ca0/ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d", size = 10853584, upload-time = "2025-03-07T15:26:56.104Z" }, + { url = "https://files.pythonhosted.org/packages/02/49/1c79e0906b6ff551fb0894168763f705bf980864739572b2815ecd3c9df0/ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d", size = 10155692, upload-time = "2025-03-07T15:27:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/85e8082e41585e0e1ceb11e41c054e9e36fed45f4b210991052d8a75089f/ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c", size = 10369760, upload-time = "2025-03-07T15:27:04.023Z" }, + { url = "https://files.pythonhosted.org/packages/a1/90/0bc60bd4e5db051f12445046d0c85cc2c617095c0904f1aa81067dc64aea/ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e", size = 9912196, upload-time = "2025-03-07T15:27:06.93Z" }, + { url = "https://files.pythonhosted.org/packages/66/ea/0b7e8c42b1ec608033c4d5a02939c82097ddcb0b3e393e4238584b7054ab/ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12", size = 11434985, upload-time = "2025-03-07T15:27:10.082Z" }, + { url = "https://files.pythonhosted.org/packages/d5/86/3171d1eff893db4f91755175a6e1163c5887be1f1e2f4f6c0c59527c2bfd/ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16", size = 12155842, upload-time = "2025-03-07T15:27:12.727Z" }, + { url = "https://files.pythonhosted.org/packages/89/9e/700ca289f172a38eb0bca752056d0a42637fa17b81649b9331786cb791d7/ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52", size = 11613804, upload-time = "2025-03-07T15:27:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/f2/92/648020b3b5db180f41a931a68b1c8575cca3e63cec86fd26807422a0dbad/ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1", size = 13823776, upload-time = "2025-03-07T15:27:18.996Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/cc472161cd04d30a09d5c90698696b70c169eeba2c41030344194242db45/ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c", size = 11302673, upload-time = "2025-03-07T15:27:21.655Z" }, + { url = "https://files.pythonhosted.org/packages/6c/db/d31c361c4025b1b9102b4d032c70a69adb9ee6fde093f6c3bf29f831c85c/ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43", size = 10235358, upload-time = "2025-03-07T15:27:24.72Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/d6374e24a14d4d93ebe120f45edd82ad7dcf3ef999ffc92b197d81cdc2a5/ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c", size = 9886177, upload-time = "2025-03-07T15:27:27.282Z" }, + { url = "https://files.pythonhosted.org/packages/00/62/a61691f6eaaac1e945a1f3f59f1eea9a218513139d5b6c2b8f88b43b5b8f/ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5", size = 10864747, upload-time = "2025-03-07T15:27:30.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/94/2c7065e1d92a8a8a46d46d9c3cf07b0aa7e0a1e0153d74baa5e6620b4102/ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8", size = 11360441, upload-time = "2025-03-07T15:27:33.356Z" }, + { url = "https://files.pythonhosted.org/packages/a7/8f/1f545ea6f9fcd7bf4368551fb91d2064d8f0577b3079bb3f0ae5779fb773/ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029", size = 10247401, upload-time = "2025-03-07T15:27:35.994Z" }, + { url = "https://files.pythonhosted.org/packages/4f/18/fb703603ab108e5c165f52f5b86ee2aa9be43bb781703ec87c66a5f5d604/ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1", size = 11366360, upload-time = "2025-03-07T15:27:38.66Z" }, + { url = "https://files.pythonhosted.org/packages/35/85/338e603dc68e7d9994d5d84f24adbf69bae760ba5efd3e20f5ff2cec18da/ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69", size = 10436892, upload-time = "2025-03-07T15:27:41.687Z" }, ] [[package]]