-
Notifications
You must be signed in to change notification settings - Fork 1.5k
containerapp compose-for-agents support #9422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
simonjj
wants to merge
10
commits into
Azure:main
Choose a base branch
from
simonjj:ai-compose-refactor
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
a139d1d
feat(containerapp): Add Docker Compose models and MCP gateway support
96cec3b
feat(containerapp): Enhance compose deployment with GPU and ingress i…
004c93f
fix: Address PR review feedback
815954b
fix: Resolve CI linting failures
3337a03
added app logging and cleaup
b72dde6
improving aca env logging
d648801
logging improvements
f08f8f7
adding secret parsing and respective logging
638b6b1
introducting local build capability
80a128b
fixing error for secrets, suppressing wrong logging
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
| """Helpers ported from azure-cli core compose utilities. | ||
|
|
||
| This module contains logic copied from azure-cli commits: | ||
| - 092e028c556c5d98c06ea1a337c26b97fe00ce59 | ||
| - 2f7ef21a0d6c4afb9f066c0d65affcc84a8b36a4 | ||
|
|
||
| The implementations are kept in the extension to avoid depending on | ||
| those specific core revisions. Keep in sync with CLI >= 2.78.0. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Dict, Iterable, List | ||
|
|
||
| from knack.log import get_logger | ||
|
|
||
| LOGGER = get_logger(__name__) | ||
|
|
||
|
|
||
| def parse_models_section(compose_yaml: Dict) -> Dict[str, Dict]: | ||
| """Ported from 092e028c556c5d98c06ea1a337c26b97fe00ce59. | ||
|
|
||
| Extract the ``models`` block from the docker-compose YAML and normalise the | ||
| structure so downstream helpers can reason about model metadata. | ||
| """ | ||
| models: Dict[str, Dict] = {} | ||
| if "models" not in compose_yaml or compose_yaml["models"] is None: | ||
| return models | ||
|
|
||
| models_section = compose_yaml["models"] | ||
| for model_name, model_config in models_section.items(): | ||
| if isinstance(model_config, dict): | ||
| # Pass through all keys except x-azure-deployment (which is handled separately) | ||
| # This preserves keys like runtime_flags, model, etc. | ||
| models[model_name] = {k: v for k, v in model_config.items() if k != 'x-azure-deployment'} | ||
| elif isinstance(model_config, str): | ||
| models[model_name] = { | ||
| "model": model_config, | ||
| } | ||
|
|
||
| if models: | ||
| LOGGER.info("Ported models section parser found %s model(s)", len(models)) | ||
| return models | ||
|
|
||
|
|
||
| def parse_service_models_config(service) -> Dict[str, Dict[str, str]]: | ||
| """Ported from 092e028c556c5d98c06ea1a337c26b97fe00ce59. | ||
|
|
||
| The original helper returns everything under ``service.models`` unchanged | ||
| when it is a mapping. This keeps per-service overrides intact. | ||
| """ | ||
| if not hasattr(service, "models") or service.models is None: | ||
| return {} | ||
| if not isinstance(service.models, dict): | ||
| return {} | ||
| return service.models | ||
|
|
||
|
|
||
| def detect_service_type(service) -> str: | ||
| """Ported from 092e028c556c5d98c06ea1a337c26b97fe00ce59. | ||
|
|
||
| Classify a compose service so that compose processing can customise | ||
| behaviour for MCP gateway, model-runner, agent, or generic services. | ||
| """ | ||
| service_name = service.name.lower() if hasattr(service, "name") else "" | ||
| image_name = service.image.lower() if getattr(service, "image", None) else "" | ||
| command_str = "" | ||
| if getattr(service, "command", None) is not None: | ||
| command = service.command | ||
| command_str = command.command_string().lower() if hasattr(command, "command_string") else str(command).lower() | ||
|
|
||
| if "mcp-gateway" in service_name or "mcp-gateway" in image_name or "--servers" in command_str: | ||
| return "mcp-gateway" | ||
| if "model-runner" in service_name or "model-runner" in image_name: | ||
| return "model-runner" | ||
| if hasattr(service, "models") and service.models: | ||
| return "agent" | ||
| if hasattr(service, "depends_on") and service.depends_on: | ||
| depends_on_iter = service.depends_on | ||
| if isinstance(depends_on_iter, dict): | ||
| depends_on_iter = depends_on_iter.keys() | ||
| for dependency in depends_on_iter: | ||
| dep_str = str(dependency).lower() | ||
| if "mcp-gateway" in dep_str or "model-runner" in dep_str: | ||
| return "agent" | ||
| return "generic" | ||
|
|
||
|
|
||
| def parse_mcp_servers_from_command(service) -> List[Dict[str, object]]: | ||
| """Ported from 092e028c556c5d98c06ea1a337c26b97fe00ce59. | ||
|
|
||
| Inspect the MCP gateway command line for ``--servers``/``--tools`` flags | ||
| and return a normalised list of server definitions. | ||
| """ | ||
| if getattr(service, "command", None) is None: | ||
| return [] | ||
|
|
||
| command = service.command | ||
| command_str = command.command_string() if hasattr(command, "command_string") else str(command) | ||
| command_parts = command_str.split() | ||
|
|
||
| servers: List[str] = [] | ||
| tools: List[str] = [] | ||
| for idx, part in enumerate(command_parts): | ||
| if part == "--servers" and idx + 1 < len(command_parts): | ||
| servers = [item.strip() for item in command_parts[idx + 1].split(",") if item.strip()] | ||
| if part == "--tools" and idx + 1 < len(command_parts): | ||
| tools = [item.strip() for item in command_parts[idx + 1].split(",") if item.strip()] | ||
|
|
||
| return [ | ||
| { | ||
| "name": server_name, | ||
| "server_type": server_name, | ||
| "tools": tools if tools else ["*"], | ||
| "image": f"docker/mcp-server-{server_name}", | ||
| "resources": {"cpu": "0.5", "memory": "1.0"}, | ||
| } | ||
| for server_name in servers | ||
| ] | ||
|
|
||
|
|
||
| def should_deploy_model_runner(compose_yaml: Dict, parsed_compose_file) -> bool: | ||
| """Ported from 092e028c556c5d98c06ea1a337c26b97fe00ce59.""" | ||
| if compose_yaml.get("models"): | ||
| return True | ||
| for service in getattr(parsed_compose_file, "services", {}).values(): | ||
| if hasattr(service, "models") and service.models: | ||
| return True | ||
| return False | ||
|
|
||
|
|
||
| def get_model_runner_environment_vars(models_config: Dict, aca_environment_name: str) -> List[str]: | ||
| """Ported from 092e028c556c5d98c06ea1a337c26b97fe00ce59.""" | ||
| if not aca_environment_name: | ||
| return [] | ||
| base = f"http://model-runner.internal.{aca_environment_name}.azurecontainerapps.io:8080" | ||
| return ["MODEL_RUNNER_URL=" + base] | ||
|
|
||
|
|
||
| def get_mcp_gateway_environment_vars(aca_environment_name: str) -> List[str]: | ||
| """Ported from 092e028c556c5d98c06ea1a337c26b97fe00ce59.""" | ||
| if not aca_environment_name: | ||
| return [] | ||
| base = f"http://mcp-gateway.internal.{aca_environment_name}.azurecontainerapps.io:8811" | ||
| return [ | ||
| "MCP_GATEWAY_URL=" + base, | ||
| "MCPGATEWAY_ENDPOINT=" + base + "/sse", | ||
| ] | ||
|
|
||
|
|
||
| def extract_model_definitions(compose_yaml: Dict, parsed_compose_file) -> List[Dict[str, object]]: | ||
| """Ported from 092e028c556c5d98c06ea1a337c26b97fe00ce59.""" | ||
| definitions: List[Dict[str, object]] = [] | ||
| models = parse_models_section(compose_yaml) | ||
|
|
||
| endpoint_var_mapping: Dict[str, List[str]] = {} | ||
| for service in getattr(parsed_compose_file, "services", {}).values(): | ||
| service_models = parse_service_models_config(service) | ||
| for model_ref, model_config in service_models.items(): | ||
| if not isinstance(model_config, dict): | ||
| continue | ||
| endpoint_var = model_config.get("endpoint_var") | ||
| model_var = model_config.get("model_var") | ||
| endpoint_var_mapping.setdefault(model_ref, []) | ||
| if endpoint_var: | ||
| endpoint_var_mapping[model_ref].append(endpoint_var) | ||
| if model_var: | ||
| endpoint_var_mapping[model_ref].append(model_var) | ||
|
|
||
| for model_name, model_config in models.items(): | ||
| definition = { | ||
| "name": model_name, | ||
| "model": model_config.get("model"), | ||
| "volume": model_config.get("volume"), | ||
| "context_size": model_config.get("context_size"), | ||
| "gpu": model_config.get("gpu", False), | ||
| "endpoint_vars": endpoint_var_mapping.get(model_name, []), | ||
| } | ||
| definitions.append(definition) | ||
|
|
||
| return definitions | ||
|
|
||
|
|
||
| def get_model_endpoint_environment_vars( | ||
| service_models: Dict[str, Dict[str, str]], | ||
| models_config: Dict[str, Dict[str, object]], | ||
| aca_environment_name: str, | ||
| ) -> List[str]: | ||
| """Ported from 092e028c556c5d98c06ea1a337c26b97fe00ce59.""" | ||
| env_vars: List[str] = [] | ||
| if not service_models or not isinstance(service_models, dict): | ||
| return env_vars | ||
|
|
||
| base_url = f"http://model-runner.internal.{aca_environment_name}.azurecontainerapps.io:8080" | ||
| for model_ref, model_config in service_models.items(): | ||
| if not isinstance(model_config, dict): | ||
| continue | ||
| endpoint_var = model_config.get("endpoint_var") | ||
| model_var = model_config.get("model_var") | ||
| model_name = None | ||
| if models_config and model_ref in models_config: | ||
| model_name = models_config[model_ref].get("model") | ||
| if endpoint_var: | ||
| env_vars.append(f"{endpoint_var}={base_url}/v1/chat/completions") | ||
| if model_var and model_name: | ||
| env_vars.append(f"{model_var}={model_name}") | ||
| return env_vars | ||
|
|
||
|
|
||
| def calculate_model_runner_resources(model_definitions: Iterable[Dict[str, object]]) -> tuple[str, str]: | ||
| """Ported from 092e028c556c5d98c06ea1a337c26b97fe00ce59. | ||
|
|
||
| Mirrors the upstream helper by returning a ``(cpu, memory)`` tuple as | ||
| strings. | ||
| """ | ||
| definitions = list(model_definitions) | ||
| if not definitions: | ||
| return "1.0", "4.0" | ||
|
|
||
| base_cpu = 1.0 | ||
| base_memory = 4.0 | ||
| if any(definition.get("gpu", False) for definition in definitions): | ||
| base_cpu = 2.0 | ||
| base_memory = 8.0 | ||
|
|
||
| extra_models = max(0, len(definitions) - 1) | ||
| if extra_models: | ||
| base_cpu = min(4.0, base_cpu + extra_models * 0.5) | ||
| base_memory = min(16.0, base_memory + extra_models * 2.0) | ||
|
|
||
| return str(base_cpu), str(base_memory) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have any changes in these functions? Do you mean some of them changed and some are just copy?
Recommend to import for
azure-clidirectly