Spaces:
Sleeping
Sleeping
File size: 6,869 Bytes
595d1ed | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | import os
import urllib.parse
import boto3
ecs = boto3.client("ecs")
s3 = boto3.client("s3")
# Move static config to Lambda env vars for easier ops
BUCKET = os.environ.get("BUCKET", "lambeth-llm-topic-modelling")
# Durable config (app_defaults.env) lives on the log/config bucket so it is not
# deleted by the output bucket object-expiration lifecycle rule.
LOG_BUCKET = os.environ.get("LOG_BUCKET") or BUCKET
INPUT_PREFIX = os.environ.get("INPUT_PREFIX", "input/")
ENV_PREFIX = os.environ.get("ENV_PREFIX", f"{INPUT_PREFIX}config/")
GENERAL_ENV_PREFIX = os.environ.get("GENERAL_ENV_PREFIX", "general-config/")
ENV_SUFFIX = os.environ.get("ENV_SUFFIX", ".env")
DEFAULT_PARAMS_KEY = os.environ.get(
"DEFAULT_PARAMS_KEY", f"{GENERAL_ENV_PREFIX}app_defaults{ENV_SUFFIX}"
)
CLUSTER = os.environ.get("ECS_CLUSTER", "analytics_fargate_cluster")
TASK_DEF = os.environ.get("ECS_TASK_DEF", "llm_topic_modelling_fargate_def:5")
SUBNETS = os.environ.get(
"SUBNETS", "subnet-083650a35f93a0ee5,subnet-0a41ba70470cabab8"
).split(",")
SECURITY_GROUPS = os.environ.get("SECURITY_GROUPS", "sg-051effb4c5e752df1").split(",")
ECS_ASSIGN_PUBLIC_IP = os.environ.get("ECS_ASSIGN_PUBLIC_IP", "DISABLED").upper()
DEFAULT_INPUT_S3_URI = os.environ.get(
"DEFAULT_INPUT_S3_URI",
f"s3://{BUCKET}/{INPUT_PREFIX}dummy_consultation_response.xlsx",
)
DEFAULT_TASK_TYPE = os.environ.get("DEFAULT_TASK_TYPE", "extract")
CONTAINER_NAME = os.environ.get("CONTAINER_NAME", "llm_topic_modelling_container")
def _key_matches(key: str) -> bool:
return key.startswith(ENV_PREFIX) and key.endswith(ENV_SUFFIX)
def _derive_runtime_params_from_key(key: str) -> dict:
"""Optional convention: decide task type from file name."""
basename = key.split("/")[-1].lower()
if "train" in basename:
task_type = "train"
elif "infer" in basename or "staging" in basename:
task_type = "infer"
else:
task_type = DEFAULT_TASK_TYPE
return {
"DIRECT_MODE_TASK": task_type,
"DIRECT_MODE_INPUT_FILE": DEFAULT_INPUT_S3_URI,
}
def _parse_dotenv(dotenv_bytes: bytes, bucket: str, input_prefix: str) -> dict:
"""
Parse a basic .env and prepend S3 paths to specific keys if they have values.
"""
env = {}
text = dotenv_bytes.decode("utf-8", errors="replace")
# 1. Parse the file (Existing Logic)
for raw_line in text.splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("export "):
line = line[len("export ") :].strip()
if "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
value = value.strip()
if (value.startswith('"') and value.endswith('"')) or (
value.startswith("'") and value.endswith("'")
):
value = value[1:-1]
env[key] = value
# 2. Apply S3 Prefix Logic (New Logic)
# We define the base S3 path string using the provided arguments
s3_base = f"s3://{bucket}/{input_prefix}"
# List of keys that need modification
keys_to_modify = ["DIRECT_MODE_INPUT_FILE", "DIRECT_MODE_CANDIDATE_TOPICS"]
for key in keys_to_modify:
# env.get(key) returns the value.
# The 'if' check ensures the key exists AND is not an empty string.
if env.get(key):
env[key] = s3_base + env[key]
return env
def _build_environment_array(*env_dicts):
"""Merge dictionaries left→right and produce ECS environment array format."""
merged = {}
for d in env_dicts:
merged.update(d or {})
# ECS override format: [{"name": "...","value":"..."}]
return [{"name": k, "value": v} for k, v in merged.items()]
def lambda_handler(event, context):
runs = []
# Parse default env file with default config.
# Read from LOG_BUCKET (log/config bucket) so app_defaults.env survives
# output-bucket lifecycle expiration.
print(f"Loading default params from s3://{LOG_BUCKET}/{DEFAULT_PARAMS_KEY}")
default_obj = s3.get_object(Bucket=LOG_BUCKET, Key=DEFAULT_PARAMS_KEY)
default_dotenv_bytes = default_obj["Body"].read()
# Parse .env → dict of env vars (input paths still resolve against output BUCKET)
default_file_env = _parse_dotenv(default_dotenv_bytes, BUCKET, INPUT_PREFIX)
for record in event.get("Records", []):
# Parse env file from user
s3rec = record.get("s3", {})
bucket = s3rec.get("bucket", {}).get("name")
raw_key = s3rec.get("object", {}).get("key")
if not bucket or not raw_key:
print(f"Object in bucket {bucket} and key {raw_key} not found, exiting.")
continue
key = urllib.parse.unquote_plus(raw_key)
# combined_key = ENV_PREFIX + key
combined_key = key
print("combined_key:", combined_key)
# Only process the intended prefix/suffix
if not _key_matches(combined_key):
print(f"Key does not match: {combined_key}, exiting")
continue
# Fetch the .env file content from S3
obj = s3.get_object(Bucket=bucket, Key=combined_key)
dotenv_bytes = obj["Body"].read()
# Parse .env → dict of env vars
file_env = _parse_dotenv(dotenv_bytes, BUCKET, INPUT_PREFIX)
# Optionally derive run-time params from naming
# run_params = _derive_runtime_params_from_key(key)
# Merge: file env (dominant), overwrites default values.
environment = _build_environment_array(file_env, default_file_env)
print("Combined environment variables:", environment)
# Execute ECS Fargate task with fully dynamic environment overrides
response = ecs.run_task(
cluster=CLUSTER,
launchType="FARGATE",
taskDefinition=TASK_DEF,
networkConfiguration={
"awsvpcConfiguration": {
"subnets": SUBNETS,
"securityGroups": SECURITY_GROUPS,
"assignPublicIp": (
ECS_ASSIGN_PUBLIC_IP
if ECS_ASSIGN_PUBLIC_IP in ("ENABLED", "DISABLED")
else "DISABLED"
),
}
},
overrides={
"containerOverrides": [
{
"name": CONTAINER_NAME,
"environment": environment,
}
]
},
)
runs.append(
{
"bucket": bucket,
"key": key,
"taskArns": [t["taskArn"] for t in response.get("tasks", [])],
"failures": response.get("failures", []),
"envCount": len(environment),
}
)
return {"runs": runs}
|