| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 35fc4b8 commit 9a8cf39
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,266 @@ | |||
| 1 | + """Pure helm-values generation for the ``agentex-agent`` chart. | ||
| 2 | + | ||
| 3 | + Maps an :class:`~agentex.config.agent_manifest.AgentManifest` plus an optional | ||
| 4 | + :class:`~agentex.config.environment_config.AgentEnvironmentConfig` to the values | ||
| 5 | + dict the ``agentex-agent`` helm chart consumes. Depends only on pydantic and the | ||
| 6 | + stdlib, so it is safe to import from a slim REST-only install without the ADK | ||
| 7 | + runtime — the same contract as the other ``agentex.config`` modules. | ||
| 8 | + | ||
| 9 | + Filesystem-aware ACP module resolution stays in | ||
| 10 | + ``agentex.lib.cli.utils.path_utils``: callers that have the agent source tree on | ||
| 11 | + disk should resolve the module themselves and pass ``acp_module``. Callers | ||
| 12 | + without one (e.g. server-side deployers) get :func:`derive_acp_module`'s | ||
| 13 | + pure-string derivation by default. | ||
| 14 | + """ | ||
| 15 | + | ||
| 16 | + from __future__ import annotations | ||
| 17 | + | ||
| 18 | + import json | ||
| 19 | + import base64 | ||
| 20 | + import logging | ||
| 21 | + from typing import Any | ||
| 22 | + | ||
| 23 | + from agentex.config.agent_manifest import AgentManifest | ||
| 24 | + from agentex.config.environment_config import AgentAuthConfig, AgentEnvironmentConfig | ||
| 25 | + | ||
| 26 | + logger = logging.getLogger(__name__) | ||
| 27 | + | ||
| 28 | + TEMPORAL_WORKER_KEY = "temporal-worker" | ||
| 29 | + AUTH_PRINCIPAL_ENV_VAR = "AUTH_PRINCIPAL_B64" | ||
| 30 | + | ||
| 31 | + __all__ = [ | ||
| 32 | + "AUTH_PRINCIPAL_ENV_VAR", | ||
| 33 | + "TEMPORAL_WORKER_KEY", | ||
| 34 | + "build_acp_command", | ||
| 35 | + "derive_acp_module", | ||
| 36 | + "encode_principal_context", | ||
| 37 | + "convert_env_vars_dict_to_list", | ||
| 38 | + "merge_deployment_configs", | ||
| 39 | + ] | ||
| 40 | + | ||
| 41 | + | ||
| 42 | + def convert_env_vars_dict_to_list(env_vars: dict[str, str]) -> list[dict[str, str]]: | ||
| 43 | + """Convert a dictionary of environment variables to a list of dictionaries""" | ||
| 44 | + return [{"name": key, "value": value} for key, value in env_vars.items()] | ||
| 45 | + | ||
| 46 | + | ||
| 47 | + def encode_principal_context(auth_config: AgentAuthConfig | None) -> str | None: | ||
| 48 | + """Base64-encode the auth principal as compact JSON, or None if unset.""" | ||
| 49 | + if auth_config is None: | ||
| 50 | + return None | ||
| 51 | + | ||
| 52 | + principal = auth_config.principal | ||
| 53 | + if not principal: | ||
| 54 | + return None | ||
| 55 | + | ||
| 56 | + json_str = json.dumps(principal, separators=(",", ":")) | ||
| 57 | + return base64.b64encode(json_str.encode("utf-8")).decode("utf-8") | ||
| 58 | + | ||
| 59 | + | ||
| 60 | + def derive_acp_module(manifest: AgentManifest) -> str: | ||
| 61 | + """Derive the ACP module from the manifest by pure string transform. | ||
| 62 | + | ||
| 63 | + Callers with the agent source tree on disk should prefer the filesystem-aware | ||
| 64 | + resolution in ``agentex.lib.cli.utils.path_utils.calculate_docker_acp_module`` | ||
| 65 | + and pass its result to :func:`merge_deployment_configs` as ``acp_module``. | ||
| 66 | + """ | ||
| 67 | + if manifest.local_development and manifest.local_development.paths: | ||
| 68 | + acp_path = manifest.local_development.paths.acp | ||
| 69 | + if acp_path: | ||
| 70 | + return acp_path.replace(".py", "").replace("/", ".") | ||
| 71 | + return "project.acp" | ||
| 72 | + | ||
| 73 | + | ||
| 74 | + def build_acp_command(acp_module: str) -> list[str]: | ||
| 75 | + """Build the uvicorn command that runs the agent's ACP server.""" | ||
| 76 | + return ["uvicorn", f"{acp_module}:acp", "--host", "0.0.0.0", "--port", "8000"] | ||
| 77 | + | ||
| 78 | + | ||
| 79 | + def _deep_merge(base_dict: dict[str, Any], override_dict: dict[str, Any]) -> None: | ||
| 80 | + """Deep merge override_dict into base_dict""" | ||
| 81 | + for key, value in override_dict.items(): | ||
| 82 | + if key in base_dict and isinstance(base_dict[key], dict) and isinstance(value, dict): | ||
| 83 | + _deep_merge(base_dict[key], value) | ||
| 84 | + else: | ||
| 85 | + base_dict[key] = value | ||
| 86 | + | ||
| 87 | + | ||
| 88 | + def merge_deployment_configs( | ||
| 89 | + manifest: AgentManifest, | ||
| 90 | + agent_env_config: AgentEnvironmentConfig | None, | ||
| 91 | + *, | ||
| 92 | + repository: str, | ||
| 93 | + image_tag: str, | ||
| 94 | + acp_module: str | None = None, | ||
| 95 | + ) -> dict[str, Any]: | ||
| 96 | + """Merge global deployment config with environment-specific overrides into helm values. | ||
| 97 | + | ||
| 98 | + Args: | ||
| 99 | + manifest: The agent manifest configuration. | ||
| 100 | + agent_env_config: Environment-specific configuration (optional). | ||
| 101 | + repository: Container image repository to deploy. | ||
| 102 | + image_tag: Container image tag to deploy. | ||
| 103 | + acp_module: Pre-resolved ACP module for the uvicorn command. Defaults to | ||
| 104 | + :func:`derive_acp_module`'s pure-string derivation. | ||
| 105 | + | ||
| 106 | + Returns: | ||
| 107 | + Dictionary of helm values ready for deployment. | ||
| 108 | + | ||
| 109 | + Raises: | ||
| 110 | + ValueError: If deployment configuration is missing or invalid. | ||
| 111 | + """ | ||
| 112 | + agent_config = manifest.agent | ||
| 113 | + | ||
| 114 | + if not manifest.deployment: | ||
| 115 | + raise ValueError("No deployment configuration found in manifest") | ||
| 116 | + | ||
| 117 | + if not repository or not image_tag: | ||
| 118 | + raise ValueError("Repository and image tag are required") | ||
| 119 | + | ||
| 120 | + # Start with global configuration | ||
| 121 | + helm_values: dict[str, Any] = { | ||
| 122 | + "global": { | ||
| 123 | + "image": { | ||
| 124 | + "repository": repository, | ||
| 125 | + "tag": image_tag, | ||
| 126 | + "pullPolicy": "IfNotPresent", | ||
| 127 | + }, | ||
| 128 | + "agent": { | ||
| 129 | + "name": manifest.agent.name, | ||
| 130 | + "description": manifest.agent.description, | ||
| 131 | + "acp_type": manifest.agent.acp_type, | ||
| 132 | + }, | ||
| 133 | + }, | ||
| 134 | + "replicaCount": manifest.deployment.global_config.replicaCount, | ||
| 135 | + "resources": { | ||
| 136 | + "requests": { | ||
| 137 | + "cpu": manifest.deployment.global_config.resources.requests.cpu, | ||
| 138 | + "memory": manifest.deployment.global_config.resources.requests.memory, | ||
| 139 | + }, | ||
| 140 | + "limits": { | ||
| 141 | + "cpu": manifest.deployment.global_config.resources.limits.cpu, | ||
| 142 | + "memory": manifest.deployment.global_config.resources.limits.memory, | ||
| 143 | + }, | ||
| 144 | + }, | ||
| 145 | + # Enable autoscaling by default for production deployments | ||
| 146 | + "autoscaling": { | ||
| 147 | + "enabled": True, | ||
| 148 | + "minReplicas": 1, | ||
| 149 | + "maxReplicas": 10, | ||
| 150 | + "targetCPUUtilizationPercentage": 50, | ||
| 151 | + }, | ||
| 152 | + } | ||
| 153 | + | ||
| 154 | + # Handle temporal configuration using new helper methods | ||
| 155 | + if agent_config.is_temporal_agent(): | ||
| 156 | + temporal_config = agent_config.get_temporal_workflow_config() | ||
| 157 | + if temporal_config: | ||
| 158 | + helm_values[TEMPORAL_WORKER_KEY] = { | ||
| 159 | + "enabled": True, | ||
| 160 | + # Enable autoscaling for temporal workers as well | ||
| 161 | + "autoscaling": { | ||
| 162 | + "enabled": True, | ||
| 163 | + "minReplicas": 1, | ||
| 164 | + "maxReplicas": 10, | ||
| 165 | + "targetCPUUtilizationPercentage": 50, | ||
| 166 | + }, | ||
| 167 | + } | ||
| 168 | + helm_values["global"]["workflow"] = { | ||
| 169 | + "name": temporal_config.name, | ||
| 170 | + "taskQueue": temporal_config.queue_name, | ||
| 171 | + } | ||
| 172 | + | ||
| 173 | + # Collect all environment variables with proper precedence | ||
| 174 | + # Priority: manifest -> environments.yaml -> secrets (highest) | ||
| 175 | + all_env_vars: dict[str, str] = {} | ||
| 176 | + secret_env_vars: list[dict[str, str]] = [] | ||
| 177 | + | ||
| 178 | + # Start with agent_config env vars from manifest | ||
| 179 | + if agent_config.env: | ||
| 180 | + all_env_vars.update(agent_config.env) | ||
| 181 | + | ||
| 182 | + # Override with environment config env vars if they exist | ||
| 183 | + if agent_env_config and agent_env_config.helm_overrides and "env" in agent_env_config.helm_overrides: | ||
| 184 | + env_overrides = agent_env_config.helm_overrides["env"] | ||
| 185 | + if isinstance(env_overrides, list): | ||
| 186 | + # Convert list format to dict for easier merging | ||
| 187 | + env_override_dict: dict[str, str] = {} | ||
| 188 | + for env_var in env_overrides: | ||
| 189 | + if isinstance(env_var, dict) and "name" in env_var and "value" in env_var: | ||
| 190 | + env_override_dict[str(env_var["name"])] = str(env_var["value"]) | ||
| 191 | + all_env_vars.update(env_override_dict) | ||
| 192 | + | ||
| 193 | + # Handle credentials and check for conflicts | ||
| 194 | + if agent_config.credentials: | ||
| 195 | + for credential in agent_config.credentials: | ||
| 196 | + # Handle both CredentialMapping objects and legacy dict format | ||
| 197 | + if isinstance(credential, dict): | ||
| 198 | + env_var_name = credential["env_var_name"] | ||
| 199 | + secret_name = credential["secret_name"] | ||
| 200 | + secret_key = credential["secret_key"] | ||
| 201 | + else: | ||
| 202 | + env_var_name = credential.env_var_name | ||
| 203 | + secret_name = credential.secret_name | ||
| 204 | + secret_key = credential.secret_key | ||
| 205 | + | ||
| 206 | + # Check if the environment variable name conflicts with existing env vars | ||
| 207 | + if env_var_name in all_env_vars: | ||
| 208 | + logger.warning( | ||
| 209 | + f"Environment variable '{env_var_name}' is defined in both " | ||
| 210 | + f"env and secretEnvVars. The secret value will take precedence." | ||
| 211 | + ) | ||
| 212 | + # Remove from regular env vars since secret takes precedence | ||
| 213 | + del all_env_vars[env_var_name] | ||
| 214 | + | ||
| 215 | + secret_env_vars.append( | ||
| 216 | + { | ||
| 217 | + "name": env_var_name, | ||
| 218 | + "secretName": secret_name, | ||
| 219 | + "secretKey": secret_key, | ||
| 220 | + } | ||
| 221 | + ) | ||
| 222 | + | ||
| 223 | + # Apply agent environment configuration overrides | ||
| 224 | + if agent_env_config: | ||
| 225 | + # Add auth principal env var if environment config is set | ||
| 226 | + if agent_env_config.auth: | ||
| 227 | + encoded_principal = encode_principal_context(agent_env_config.auth) | ||
| 228 | + if encoded_principal: | ||
| 229 | + all_env_vars[AUTH_PRINCIPAL_ENV_VAR] = encoded_principal | ||
| 230 | + else: | ||
| 231 | + raise ValueError(f"Auth principal unable to be encoded for agent_env_config: {agent_env_config}") | ||
| 232 | + | ||
| 233 | + if agent_env_config.helm_overrides: | ||
| 234 | + _deep_merge(helm_values, agent_env_config.helm_overrides) | ||
| 235 | + | ||
| 236 | + # Set final environment variables | ||
| 237 | + # Environment variable precedence: manifest -> environments.yaml -> secrets (highest) | ||
| 238 | + if all_env_vars: | ||
| 239 | + helm_values["env"] = convert_env_vars_dict_to_list(all_env_vars) | ||
| 240 | + | ||
| 241 | + if secret_env_vars: | ||
| 242 | + helm_values["secretEnvVars"] = secret_env_vars | ||
| 243 | + | ||
| 244 | + # Set environment variables for temporal worker if enabled | ||
| 245 | + if TEMPORAL_WORKER_KEY in helm_values: | ||
| 246 | + if all_env_vars: | ||
| 247 | + helm_values[TEMPORAL_WORKER_KEY]["env"] = convert_env_vars_dict_to_list(all_env_vars) | ||
| 248 | + if secret_env_vars: | ||
| 249 | + helm_values[TEMPORAL_WORKER_KEY]["secretEnvVars"] = secret_env_vars | ||
| 250 | + | ||
| 251 | + # Handle image pull secrets | ||
| 252 | + if manifest.deployment and manifest.deployment.imagePullSecrets: | ||
| 253 | + pull_secrets = [pull_secret.model_dump() for pull_secret in manifest.deployment.imagePullSecrets] | ||
| 254 | + helm_values["global"]["imagePullSecrets"] = pull_secrets | ||
| 255 | + helm_values["imagePullSecrets"] = pull_secrets | ||
| 256 | + | ||
| 257 | + # Add dynamic ACP command based on manifest configuration if command is not set in helm overrides | ||
| 258 | + helm_overrides_command = ( | ||
| 259 | + agent_env_config and agent_env_config.helm_overrides and "command" in agent_env_config.helm_overrides | ||
| 260 | + ) | ||
| 261 | + if not helm_overrides_command: | ||
| 262 | + module = acp_module or derive_acp_module(manifest) | ||
| 263 | + helm_values["command"] = build_acp_command(module) | ||
| 264 | + logger.info(f"Using ACP command: uvicorn {module}:acp") | ||
| 265 | + | ||
| 266 | + return helm_values | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments