Skip to content

Commit 245b419

Browse files
Merge branch 'main' into release-2.8.5
2 parents a71285a + d4ead74 commit 245b419

14 files changed

+660
-37
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ These images come in two variants, CPU and GPU, and include deep learning framew
77
Keras; popular Python packages like numpy, scikit-learn and pandas; and IDEs like Jupyter Lab. The distribution contains
88
the _latest_ versions of all these packages _such that_ they are _mutually compatible_.
99

10+
Starting with v2.9.5+ and new v3.5+, the images include Amazon Q Agentic Chat integration for enhanced AI-powered development assistance in JupyterLab.
11+
12+
### Amazon Q Agentic Chat Integration
13+
14+
The images include pre-configured Amazon Q artifacts and shared web client libraries:
15+
- `/etc/web-client/libs/` - Shared JavaScript libraries (JSZip) for all web applications
16+
- `/etc/amazon-q-agentic-chat/artifacts/jupyterlab/` - Amazon Q server and client artifacts for JupyterLab
17+
1018
This project follows semver (more on that below) and comes with a helper tool to automate new releases of the
1119
distribution.
1220

assets/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Assets
2+
3+
This directory contains utility scripts and files used during the Docker image build process.
4+
5+
## extract_amazon_q_agentic_chat_urls.py
6+
7+
A Python script that extracts Amazon Q Agentic Chat artifact URLs from a manifest file for Linux x64 platform.
8+
9+
### Usage
10+
```bash
11+
python extract_amazon_q_agentic_chat_urls.py <manifest_file> <version>
12+
```
13+
14+
### Parameters
15+
- `manifest_file`: Path to the JSON manifest file containing artifact information
16+
- `version`: The server version to extract artifacts for
17+
18+
### Output
19+
The script outputs environment variables for use in shell scripts:
20+
- `SERVERS_URL`: URL for the servers.zip artifact
21+
- `CLIENTS_URL`: URL for the clients.zip artifact
22+
23+
## download_amazon_q_agentic_chat_artifacts.sh
24+
25+
A modular shell script that downloads and extracts Amazon Q Agentic Chat artifacts for IDE integration.
26+
27+
### Usage
28+
```bash
29+
bash download_amazon_q_agentic_chat_artifacts.sh <version> <target_dir> <ide_type>
30+
```
31+
32+
### Parameters
33+
- `version`: Amazon Q server version (defaults to $FLARE_SERVER_VERSION_JL)
34+
- `target_dir`: Target directory for artifacts (defaults to /etc/amazon-q-agentic-chat/artifacts/jupyterlab)
35+
- `ide_type`: IDE type for logging (defaults to jupyterlab)
36+
37+
### Features
38+
- Downloads JSZip library to shared web client location (/etc/web-client/libs/) for reuse across all web applications
39+
- Modular design supports future VSCode integration
40+
- Comprehensive error handling with retry logic
41+
- Automatic cleanup of temporary files
42+
43+
### Directory Structure
44+
- `/etc/web-client/libs/` - Shared web client libraries (JSZip, etc.) for any web application
45+
- `/etc/amazon-q-agentic-chat/artifacts/jupyterlab/` - Amazon Q specific artifacts for JupyterLab
46+
- `/etc/amazon-q-agentic-chat/artifacts/vscode/` - Future Amazon Q artifacts for VSCode
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Download Amazon Q artifacts for IDE integration
5+
# Usage: download_amazon_q_artifacts.sh <version> <target_dir> <ide_type>
6+
# Example: download_amazon_q_artifacts.sh 1.25.0 /etc/amazon-q/artifacts/agentic-chat jupyterlab
7+
8+
VERSION=${1:-$FLARE_SERVER_VERSION_JL}
9+
TARGET_DIR=${2:-"/etc/amazon-q-agentic-chat/artifacts/jupyterlab"}
10+
IDE_TYPE=${3:-"jupyterlab"}
11+
12+
if [ -z "$VERSION" ]; then
13+
echo "Error: Version not specified and FLARE_SERVER_VERSION_JL not set"
14+
exit 1
15+
fi
16+
17+
echo "Downloading Amazon Q artifacts for $IDE_TYPE (version: $VERSION)"
18+
19+
# Create target directories
20+
sudo mkdir -p "$TARGET_DIR"
21+
22+
# Download manifest and extract artifact URLs
23+
MANIFEST_URL="https://aws-toolkit-language-servers.amazonaws.com/qAgenticChatServer/0/manifest.json"
24+
curl -L --retry 3 --retry-delay 5 --fail "$MANIFEST_URL" -o "/tmp/manifest.json" || {
25+
echo "Failed to download manifest"
26+
exit 1
27+
}
28+
29+
# Extract artifact URLs
30+
ARTIFACT_URLS=$(python3 /tmp/extract_amazon_q_agentic_chat_urls.py /tmp/manifest.json "$VERSION")
31+
if [ $? -ne 0 ] || [ -z "$ARTIFACT_URLS" ]; then
32+
echo "Failed to extract Amazon Q artifact URLs"
33+
exit 1
34+
fi
35+
36+
eval "$ARTIFACT_URLS"
37+
38+
# Download and extract servers.zip
39+
echo "Downloading servers.zip..."
40+
curl -L --retry 3 --retry-delay 5 --fail "$SERVERS_URL" -o "/tmp/servers.zip" || {
41+
echo "Failed to download servers.zip"
42+
exit 1
43+
}
44+
sudo unzip "/tmp/servers.zip" -d "$TARGET_DIR/servers" || {
45+
echo "Failed to extract servers.zip"
46+
exit 1
47+
}
48+
49+
# Download and extract clients.zip
50+
echo "Downloading clients.zip..."
51+
curl -L --retry 3 --retry-delay 5 --fail "$CLIENTS_URL" -o "/tmp/clients.zip" || {
52+
echo "Failed to download clients.zip"
53+
exit 1
54+
}
55+
sudo unzip "/tmp/clients.zip" -d "$TARGET_DIR/clients" || {
56+
echo "Failed to extract clients.zip"
57+
exit 1
58+
}
59+
60+
# Clean up temporary files
61+
rm -f /tmp/manifest.json /tmp/servers.zip /tmp/clients.zip
62+
63+
echo "Amazon Q artifacts downloaded successfully to $TARGET_DIR"
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env python3
2+
"""Extract Amazon Q artifact URLs from manifest for Linux x64 platform."""
3+
4+
import json
5+
import sys
6+
7+
8+
def extract_urls(manifest_file, version, platform="linux", arch="x64"):
9+
"""Extract servers.zip and clients.zip URLs for specified platform/arch."""
10+
try:
11+
with open(manifest_file) as f:
12+
manifest = json.load(f)
13+
except FileNotFoundError:
14+
raise FileNotFoundError(f"Manifest file not found: {manifest_file}")
15+
except json.JSONDecodeError as e:
16+
raise ValueError(f"Invalid JSON in manifest file {manifest_file}: {str(e)}")
17+
18+
for ver in manifest["versions"]:
19+
if ver["serverVersion"] == version:
20+
for target in ver["targets"]:
21+
if target["platform"] == platform and target.get("arch") == arch:
22+
servers_url = None
23+
clients_url = None
24+
25+
for content in target["contents"]:
26+
if content["filename"] == "servers.zip":
27+
servers_url = content["url"]
28+
elif content["filename"] == "clients.zip":
29+
clients_url = content["url"]
30+
31+
if servers_url is None or clients_url is None:
32+
raise ValueError(
33+
f"Required files (servers.zip/clients.zip) not found for version {version} {platform} {arch}"
34+
)
35+
36+
return servers_url, clients_url
37+
38+
raise ValueError(f"Version {version} not found for {platform} {arch}")
39+
40+
41+
if __name__ == "__main__":
42+
if len(sys.argv) != 3:
43+
print("Usage: extract_amazon_q_agentic_chat_urls.py <manifest_file> <version>")
44+
sys.exit(1)
45+
46+
manifest_file, version = sys.argv[1], sys.argv[2]
47+
servers_url, clients_url = extract_urls(manifest_file, version)
48+
49+
print(f"SERVERS_URL={servers_url}")
50+
print(f"CLIENTS_URL={clients_url}")

src/main.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,15 @@ def _copy_static_files(base_version_dir, new_version_dir, new_version_major, run
131131
if os.path.exists(aws_cli_key_path):
132132
shutil.copy2(aws_cli_key_path, new_version_dir)
133133

134+
# Copy Amazon Q agentic chat scripts from assets
135+
q_extract_script_path = os.path.relpath(f"assets/extract_amazon_q_agentic_chat_urls.py")
136+
if os.path.exists(q_extract_script_path):
137+
shutil.copy2(q_extract_script_path, new_version_dir)
138+
139+
q_download_script_path = os.path.relpath(f"assets/download_amazon_q_agentic_chat_artifacts.sh")
140+
if os.path.exists(q_download_script_path):
141+
shutil.copy2(q_download_script_path, new_version_dir)
142+
134143
if int(new_version_major) >= 1:
135144
# dirs directory doesn't exist for v0. It was introduced only for v1
136145
dirs_relative_path = os.path.relpath(f"{base_path}/dirs")
@@ -268,7 +277,7 @@ def _build_local_images(
268277
# Minimal patch build, use .patch Dockerfiles
269278
dockerfile = f"./Dockerfile-{image_type}.patch"
270279
else:
271-
dockerfile="./Dockerfile"
280+
dockerfile = "./Dockerfile"
272281
try:
273282
image, log_gen = _docker_client.images.build(
274283
path=target_version_dir, dockerfile=dockerfile, rm=True, pull=True, buildargs=config["build_args"]

src/package_report.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import json
22
import os
3+
import subprocess
34
import warnings
45
from datetime import datetime
56
from itertools import islice
67

78
import boto3
8-
import conda.cli.python_api
99
from conda.models.match_spec import MatchSpec
1010
from condastats.cli import overall
1111
from dateutil.relativedelta import relativedelta
@@ -39,11 +39,18 @@ def _get_package_versions_in_upstream(target_packages_match_spec_out, target_ver
3939
continue
4040
channel = match_spec_out.get("channel").channel_name
4141
subdir_filter = "[subdir=" + match_spec_out.get("subdir") + "]"
42-
search_result = conda.cli.python_api.run_command(
43-
"search", channel + "::" + package + ">=" + str(package_version) + subdir_filter, "--json"
44-
)
45-
# Load the first result as json. The API sends a json string inside an array
46-
package_metadata = json.loads(search_result[0])[package]
42+
try:
43+
search_result = subprocess.run(
44+
["conda", "search", channel + "::" + package + ">=" + str(package_version) + subdir_filter, "--json"],
45+
capture_output=True,
46+
text=True,
47+
check=True,
48+
)
49+
# Load the result as json
50+
package_metadata = json.loads(search_result.stdout)[package]
51+
except (subprocess.CalledProcessError, json.JSONDecodeError, KeyError) as e:
52+
print(f"Error searching for package {package}: {str(e)}")
53+
continue
4754
# Response is of the structure
4855
# { 'package_name': [{'url':<someurl>, 'dependencies': <List of dependencies>, 'version':
4956
# <version number>}, ..., {'url':<someurl>, 'dependencies': <List of dependencies>, 'version':
@@ -90,7 +97,7 @@ def _generate_staleness_report_per_image(
9097
package_string = (
9198
package
9299
if version_in_sagemaker_distribution == package_versions_in_upstream[package]
93-
else "${\color{red}" + package + "}$"
100+
else "${\\color{red}" + package + "}$"
94101
)
95102

96103
if download_stats:
@@ -170,7 +177,7 @@ def _validate_new_package_size(new_package_total_size, target_total_size, image_
170177
+ str(new_package_total_size_percent)
171178
+ "%)"
172179
)
173-
new_package_total_size_percent_string = "${\color{red}" + str(new_package_total_size_percent) + "}$"
180+
new_package_total_size_percent_string = "${\\color{red}" + str(new_package_total_size_percent) + "}$"
174181

175182
print(
176183
"The total size of newly introduced Python packages is "
@@ -276,10 +283,13 @@ def _generate_python_package_dependency_report(image_config, base_version_dir, t
276283
for package, version in new_packages.items():
277284
try:
278285
# Pull package metadata from conda-forge and dump into json file
279-
search_result = conda.cli.python_api.run_command(
280-
"search", "-c", "conda-forge", f"{package}=={version}", "--json"
286+
search_result = subprocess.run(
287+
["conda", "search", "-c", "conda-forge", f"{package}=={version}", "--json"],
288+
capture_output=True,
289+
text=True,
290+
check=True,
281291
)
282-
package_metadata = json.loads(search_result[0])[package][0]
292+
package_metadata = json.loads(search_result.stdout)[package][0]
283293
results[package] = {"version": package_metadata["version"], "depends": package_metadata["depends"]}
284294
except Exception as e:
285295
print(f"Error in report generation: {str(e)}")

src/utils.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import json
22
import os
3+
import subprocess
34

4-
import conda.cli.python_api
5-
from conda.env.specs import RequirementsSpec
5+
from conda.env.specs.requirements import RequirementsSpec
66
from conda.exceptions import PackagesNotFoundError
77
from conda.models.match_spec import MatchSpec
88
from semver import Version
@@ -48,7 +48,8 @@ def get_semver(version_str) -> Version:
4848
return version
4949

5050

51-
def read_env_file(file_path) -> RequirementsSpec:
51+
def read_env_file(file_path):
52+
"""Read environment file using conda's RequirementsSpec"""
5253
return RequirementsSpec(filename=file_path)
5354

5455

@@ -106,9 +107,15 @@ def pull_conda_package_metadata(image_config, image_artifact_dir):
106107
if str(match_spec_out).startswith("conda-forge"):
107108
# Pull package metadata from conda-forge and dump into json file
108109
try:
109-
search_result = conda.cli.python_api.run_command("search", str(match_spec_out), "--json")
110-
package_metadata = json.loads(search_result[0])[package][0]
110+
search_result = subprocess.run(
111+
["conda", "search", str(match_spec_out), "--json"], capture_output=True, text=True, check=True
112+
)
113+
package_metadata = json.loads(search_result.stdout)[package][0]
111114
results[package] = {"version": package_metadata["version"], "size": package_metadata["size"]}
115+
except (subprocess.CalledProcessError, json.JSONDecodeError, KeyError, IndexError) as e:
116+
print(
117+
f"Failed to pull package metadata for {package}, {match_spec_out} from conda-forge, ignore. Error: {str(e)}"
118+
)
112119
except PackagesNotFoundError:
113120
print(
114121
f"Failed to pull package metadata for {package}, {match_spec_out} from conda-forge, ignore. Potentially this package is broken."

template/v2/Dockerfile

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ ARG ENV_IN_FILENAME
66
ARG PINNED_ENV_IN_FILENAME
77
ARG ARG_BASED_ENV_IN_FILENAME
88
ARG IMAGE_VERSION
9+
10+
# Amazon Q Agentic Chat version - update this default value when needed
11+
ARG FLARE_SERVER_VERSION_JL=1.25.0
12+
# IDE type for Amazon Q integration
13+
ARG AMAZON_Q_IDE_TYPE=jupyterlab
14+
915
LABEL "org.amazon.sagemaker-distribution.image.version"=$IMAGE_VERSION
1016

1117
ARG AMZN_BASE="/opt/amazon/sagemaker"
@@ -49,6 +55,8 @@ ENV MAMBA_USER=$NB_USER
4955
ENV USER=$NB_USER
5056

5157
COPY aws-cli-public-key.asc /tmp/
58+
COPY extract_amazon_q_agentic_chat_urls.py /tmp/
59+
COPY download_amazon_q_agentic_chat_artifacts.sh /tmp/
5260

5361
RUN apt-get update && apt-get upgrade -y && \
5462
apt-get install -y --no-install-recommends sudo gettext-base wget curl unzip git rsync build-essential openssh-client nano cron less mandoc jq ca-certificates gnupg && \
@@ -73,11 +81,10 @@ RUN apt-get update && apt-get upgrade -y && \
7381
unzip q.zip && \
7482
Q_INSTALL_GLOBAL=true ./q/install.sh --no-confirm && \
7583
rm -rf q q.zip && \
76-
: && \
7784
echo "source /usr/local/bin/_activate_current_env.sh" | tee --append /etc/profile && \
7885
# CodeEditor - create server, user data dirs
79-
mkdir -p /opt/amazon/sagemaker/sagemaker-code-editor-server-data /opt/amazon/sagemaker/sagemaker-code-editor-user-data \
80-
&& chown $MAMBA_USER:$MAMBA_USER /opt/amazon/sagemaker/sagemaker-code-editor-server-data /opt/amazon/sagemaker/sagemaker-code-editor-user-data && \
86+
mkdir -p /opt/amazon/sagemaker/sagemaker-code-editor-server-data /opt/amazon/sagemaker/sagemaker-ui-code-editor-server-data /opt/amazon/sagemaker/sagemaker-code-editor-user-data \
87+
&& chown $MAMBA_USER:$MAMBA_USER /opt/amazon/sagemaker/sagemaker-code-editor-server-data /opt/amazon/sagemaker/sagemaker-ui-code-editor-server-data /opt/amazon/sagemaker/sagemaker-code-editor-user-data && \
8188
# create dir to store user data files
8289
mkdir -p /opt/amazon/sagemaker/user-data \
8390
&& chown $MAMBA_USER:$MAMBA_USER /opt/amazon/sagemaker/user-data && \
@@ -87,11 +94,15 @@ COPY dirs/ ${DIRECTORY_TREE_STAGE_DIR}/
8794
RUN rsync -a ${DIRECTORY_TREE_STAGE_DIR}/ / && \
8895
rm -rf ${DIRECTORY_TREE_STAGE_DIR} && \
8996
# CodeEditor - download the extensions
90-
mkdir -p /etc/code-editor/extensions && \
97+
mkdir -p /etc/code-editor/extensions /etc/code-editor/extensions-sagemaker-ui && \
9198
while IFS= read -r url || [ -n "$url" ]; do \
9299
echo "Downloading extension from ${url}..." && \
93100
wget --no-check-certificate -P /etc/code-editor/extensions "${url}"; \
94101
done < /etc/code-editor/extensions.txt
102+
while IFS= read -r url || [ -n "$url" ]; do \
103+
echo "Downloading sagemaker-ui extension from ${url}..." && \
104+
wget --no-check-certificate -P /etc/code-editor/extensions-sagemaker-ui "${url}"; \
105+
done < /etc/code-editor/extensions-sagemaker-ui.txt
95106

96107
USER $MAMBA_USER
97108
COPY --chown=$MAMBA_USER:$MAMBA_USER $ENV_IN_FILENAME *.in /tmp/
@@ -121,6 +132,13 @@ RUN if [[ -z $ARG_BASED_ENV_IN_FILENAME ]] ; \
121132
find /opt/conda -name "yarn.lock" -type f -delete && \
122133
rm -rf /tmp/*.in && \
123134
sudo ln -s $(which python3) /usr/bin/python && \
135+
# Download shared web client libraries
136+
sudo mkdir -p /etc/web-client/libs && \
137+
sudo curl -L --retry 3 --retry-delay 5 --fail "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js" -o "/etc/web-client/libs/jszip.min.js" || (echo "Failed to download JSZip library" && exit 1) && \
138+
# Download Amazon Q Agentic Chat artifacts for JupyterLab integration
139+
bash /tmp/download_amazon_q_agentic_chat_artifacts.sh $FLARE_SERVER_VERSION_JL /etc/amazon-q-agentic-chat/artifacts/$AMAZON_Q_IDE_TYPE $AMAZON_Q_IDE_TYPE && \
140+
# Fix ownership for JupyterLab access
141+
sudo chown -R $MAMBA_USER:$MAMBA_USER /etc/amazon-q-agentic-chat/ /etc/web-client/ && \
124142
# Update npm version
125143
npm i -g npm && \
126144
# Enforce to use `conda-forge` as only channel, by removing `defaults`
@@ -133,9 +151,17 @@ RUN if [[ -z $ARG_BASED_ENV_IN_FILENAME ]] ; \
133151
echo "Installing extension ${ext}..."; \
134152
sagemaker-code-editor --install-extension "${ext}" --extensions-dir "${extensionloc}" --server-data-dir /opt/amazon/sagemaker/sagemaker-code-editor-server-data --user-data-dir /opt/amazon/sagemaker/sagemaker-code-editor-user-data; \
135153
done \
154+
# Install sagemaker-ui extensions
155+
&& extensionloc_ui=/opt/amazon/sagemaker/sagemaker-ui-code-editor-server-data/extensions && mkdir -p "${extensionloc_ui}" \
156+
&& for ext in /etc/code-editor/extensions-sagemaker-ui/*.vsix; do \
157+
echo "Installing sagemaker-ui extension ${ext}..."; \
158+
sagemaker-code-editor --install-extension "${ext}" --extensions-dir "${extensionloc_ui}" --server-data-dir /opt/amazon/sagemaker/sagemaker-ui-code-editor-server-data --user-data-dir /opt/amazon/sagemaker/sagemaker-code-editor-user-data; \
159+
done \
136160
# Copy the settings
137161
&& cp /etc/code-editor/code_editor_machine_settings.json /opt/amazon/sagemaker/sagemaker-code-editor-server-data/data/Machine/settings.json && \
138162
cp /etc/code-editor/code_editor_user_settings.json /opt/amazon/sagemaker/sagemaker-code-editor-server-data/data/User/settings.json && \
163+
cp /etc/code-editor/code_editor_machine_settings.json /opt/amazon/sagemaker/sagemaker-ui-code-editor-server-data/data/Machine/settings.json && \
164+
cp /etc/code-editor/code_editor_user_settings.json /opt/amazon/sagemaker/sagemaker-ui-code-editor-server-data/data/User/settings.json && \
139165
# Install glue kernels, and move to shared directory
140166
# Also patching base kernel so Studio background code doesn't start session silently
141167
install-glue-kernels && \

0 commit comments

Comments
 (0)