Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions agentkit/toolkit/builders/local_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,12 +317,18 @@ def build(self, config: LocalDockerBuilderConfig) -> BuildResult:
"build_script": docker_build_config.build_script,
}

def generate_dockerfile_content() -> str:
"""Generate Dockerfile content."""
from io import StringIO
from agentkit.toolkit.docker.dockerfile.metadata import (
calculate_template_hash,
)

StringIO()
template_path = Path(template_dir) / docker_config.template_name
template_hash = calculate_template_hash(template_path)

config_hash_dict["dockerfile_template"] = docker_config.template_name
config_hash_dict["dockerfile_template_hash"] = template_hash

def generate_dockerfile_content() -> str:
"""Generate Dockerfile content."""
# Use renderer to render to string
template = renderer.env.get_template(docker_config.template_name)
rendered = template.render(**context)
Expand Down
10 changes: 10 additions & 0 deletions agentkit/toolkit/builders/ve_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,16 @@ def _render_dockerfile(
"build_script": docker_build_config.build_script,
}

from agentkit.toolkit.docker.dockerfile.metadata import (
calculate_template_hash,
)

template_path = Path(template_dir) / config.dockerfile_template
template_hash = calculate_template_hash(template_path)

config_hash_dict["dockerfile_template"] = config.dockerfile_template
config_hash_dict["dockerfile_template_hash"] = template_hash

renderer = DockerfileRenderer(template_dir)

# Content generator function (captures context via closure)
Expand Down
20 changes: 20 additions & 0 deletions agentkit/toolkit/docker/dockerfile/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import hashlib
import json
import logging
from pathlib import Path
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
Expand All @@ -26,6 +27,25 @@
logger = logging.getLogger(__name__)


def calculate_template_hash(template_path: Path) -> str:
try:
return hashlib.sha256(template_path.read_bytes()).hexdigest()[:16]
except Exception as e:
version = "unknown"
try:
from agentkit.version import VERSION

version = VERSION
except Exception:
version = "unknown"

logger.warning(
"Failed to read Dockerfile template for hashing: %s (%s)", template_path, e
)
seed = f"unreadable-template:{template_path}:{version}"
return hashlib.sha256(seed.encode("utf-8")).hexdigest()[:16]


class DockerfileDecision(Enum):
"""Dockerfile 决策类型"""

Expand Down
2 changes: 1 addition & 1 deletion agentkit/toolkit/resources/templates/python/Dockerfile.j2
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{% if base_image %}
FROM {{ base_image }}
{% else %}
FROM agentkit-cn-beijing.cr.volces.com/base/py-simple:python{{ language_version }}-bookworm-slim-latest
FROM agentkit-prod-public-cn-beijing.cr.volces.com/base/py-simple:python{{ language_version }}-bookworm-slim-latest
{% endif %}

ENV UV_SYSTEM_PYTHON=1 UV_COMPILE_BYTECODE=1 PYTHONUNBUFFERED=1 DOCKER_CONTAINER=1
Expand Down
2 changes: 1 addition & 1 deletion agentkit/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.

VERSION = "0.4.5"
VERSION = "0.5.0"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "agentkit-sdk-python"
version = "0.4.5"
version = "0.5.0"
description = "Python SDK for transforming any AI agent into a production-ready application. Framework-agnostic primitives for runtime, memory, authentication, and tools with volcengine-managed infrastructure."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
155 changes: 155 additions & 0 deletions tests/toolkit/docker/test_dockerfile_manager_regeneration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
from pathlib import Path

from agentkit.toolkit.docker.dockerfile.manager import DockerfileManager
from agentkit.toolkit.docker.dockerfile.metadata import MetadataExtractor


def test_python_template_default_base_image_domain() -> None:
template_path = (
Path(__file__).resolve().parents[3]
/ "agentkit"
/ "toolkit"
/ "resources"
/ "templates"
/ "python"
/ "Dockerfile.j2"
)
content = template_path.read_text(encoding="utf-8")
assert (
"FROM agentkit-prod-public-cn-beijing.cr.volces.com/base/py-simple:python{{ language_version }}-bookworm-slim-latest"
in content
)


def test_managed_dockerfile_regenerates_when_template_hash_changes(
tmp_path: Path,
) -> None:
manager = DockerfileManager(tmp_path)

config_hash_dict_v1 = {
"language": "Python",
"language_version": "3.12",
"entry_point": "agent.py",
"dependencies_file": "requirements.txt",
"dockerfile_template": "Dockerfile.j2",
"dockerfile_template_hash": "hash_v1",
"docker_build": {"base_image": None, "build_script": None},
}

def content_generator() -> str:
return "FROM python:3.12-slim\n"

generated_1, dockerfile_path_1 = manager.prepare_dockerfile(
config_hash_dict=config_hash_dict_v1,
content_generator=content_generator,
force_regenerate=False,
)
assert generated_1 is True
assert dockerfile_path_1 == str(tmp_path / "Dockerfile")

dockerfile_content_1 = (tmp_path / "Dockerfile").read_text(encoding="utf-8")
metadata_1 = MetadataExtractor.extract(dockerfile_content_1)
assert metadata_1.is_managed is True
assert metadata_1.config_hash == MetadataExtractor.calculate_config_hash(
config_hash_dict_v1
)

config_hash_dict_v2 = dict(config_hash_dict_v1)
config_hash_dict_v2["dockerfile_template_hash"] = "hash_v2"

generated_2, dockerfile_path_2 = manager.prepare_dockerfile(
config_hash_dict=config_hash_dict_v2,
content_generator=content_generator,
force_regenerate=False,
)
assert generated_2 is True
assert dockerfile_path_2 == str(tmp_path / "Dockerfile")

backup_dir = tmp_path / ".agentkit" / "dockerfile_backups"
backups = list(backup_dir.glob("Dockerfile.backup.*"))
assert len(backups) == 1

dockerfile_content_2 = (tmp_path / "Dockerfile").read_text(encoding="utf-8")
metadata_2 = MetadataExtractor.extract(dockerfile_content_2)
assert metadata_2.is_managed is True
assert metadata_2.config_hash == MetadataExtractor.calculate_config_hash(
config_hash_dict_v2
)


def test_managed_dockerfile_not_regenerated_when_config_hash_unchanged(
tmp_path: Path,
) -> None:
manager = DockerfileManager(tmp_path)

config_hash_dict_v1 = {
"language": "Python",
"language_version": "3.12",
"entry_point": "agent.py",
"dependencies_file": "requirements.txt",
"dockerfile_template": "Dockerfile.j2",
"dockerfile_template_hash": "hash_v1",
"docker_build": {"base_image": None, "build_script": None},
}

def content_generator() -> str:
return "FROM python:3.12-slim\n"

generated_1, _ = manager.prepare_dockerfile(
config_hash_dict=config_hash_dict_v1,
content_generator=content_generator,
force_regenerate=False,
)
assert generated_1 is True

generated_2, _ = manager.prepare_dockerfile(
config_hash_dict=config_hash_dict_v1,
content_generator=content_generator,
force_regenerate=False,
)
assert generated_2 is False

backup_dir = tmp_path / ".agentkit" / "dockerfile_backups"
assert list(backup_dir.glob("Dockerfile.backup.*")) == []


def test_managed_dockerfile_not_overwritten_when_user_modified_and_config_changes(
tmp_path: Path,
) -> None:
manager = DockerfileManager(tmp_path)

config_hash_dict_v1 = {
"language": "Python",
"language_version": "3.12",
"entry_point": "agent.py",
"dependencies_file": "requirements.txt",
"dockerfile_template": "Dockerfile.j2",
"dockerfile_template_hash": "hash_v1",
"docker_build": {"base_image": None, "build_script": None},
}

def content_generator() -> str:
return "FROM python:3.12-slim\n"

manager.prepare_dockerfile(
config_hash_dict=config_hash_dict_v1,
content_generator=content_generator,
force_regenerate=False,
)

dockerfile_path = tmp_path / "Dockerfile"
dockerfile_path.write_text(
dockerfile_path.read_text(encoding="utf-8") + "\nRUN echo user-modified\n",
encoding="utf-8",
)

config_hash_dict_v2 = dict(config_hash_dict_v1)
config_hash_dict_v2["dockerfile_template_hash"] = "hash_v2"

generated, _ = manager.prepare_dockerfile(
config_hash_dict=config_hash_dict_v2,
content_generator=content_generator,
force_regenerate=False,
)
assert generated is False
assert "RUN echo user-modified" in dockerfile_path.read_text(encoding="utf-8")