/ docs / docs / documentation / developing / tutorials / building-custom-agent-images.md
building-custom-agent-images.md
  1  ---
  2  title: Building Custom Agent Images
  3  sidebar_position: 25
  4  ---
  5  
  6  # Building Custom Agent Images
  7  
  8  This tutorial walks you through packaging a custom agent plugin into a Docker/OCI image suitable for Kubernetes deployment. By the end you will have a container image that extends the SAM Enterprise base image with your own Python tool, ready to deploy with Helm. For the deployment step, see [Agent and Workflow Deployment](https://solaceproducts.github.io/solace-agent-mesh-helm-quickstart/docs/standalone-agent-deployment).
  9  
 10  ## Prerequisites
 11  
 12  You need Docker or Podman installed on your build machine, access to a container registry that your Kubernetes cluster can pull from, and a SAM Enterprise base image. Familiarity with Python packaging basics is helpful but not required---this tutorial covers everything you need. For background on tool function patterns, see [Creating Python Tools](../creating-python-tools.md). For plugin packaging basics, see [Plugins](../../components/plugins.md).
 13  
 14  ## Step 1: Create the Plugin
 15  
 16  Start by setting up a standard Python package for your custom tool. The directory structure follows the `src` layout convention:
 17  
 18  ```
 19  custom-echo-agent/
 20  ├── pyproject.toml
 21  └── src/
 22      └── custom_echo_agent/
 23          ├── __init__.py
 24          └── tools.py
 25  ```
 26  
 27  The `pyproject.toml` declares the package metadata and uses `hatchling` as the build backend. The `src-path` setting tells Hatch where to find the package source, and the `packages` list ensures only your plugin code is included in the wheel:
 28  
 29  ```toml
 30  [build-system]
 31  requires = ["hatchling"]
 32  build-backend = "hatchling.build"
 33  
 34  [project]
 35  name = "custom_echo_agent"
 36  version = "0.1.0"
 37  description = "A custom echo tool for SAM"
 38  requires-python = ">=3.10"
 39  dependencies = []
 40  
 41  [tool.hatch.build.targets.wheel]
 42  packages = ["src/custom_echo_agent"]
 43  src-path = "src"
 44  ```
 45  
 46  Create an empty `__init__.py` file in the `src/custom_echo_agent/` directory so Python recognizes it as a package.
 47  
 48  ## Step 2: Write the Tool
 49  
 50  Create `src/custom_echo_agent/tools.py` with your tool function:
 51  
 52  ```python
 53  import logging
 54  from typing import Any, Dict, Optional
 55  
 56  from google.adk.tools import ToolContext
 57  
 58  log = logging.getLogger(__name__)
 59  
 60  async def echo_tool(
 61      message: str,
 62      tool_context: Optional[ToolContext] = None,
 63      tool_config: Optional[Dict[str, Any]] = None,
 64  ) -> Dict[str, Any]:
 65      """Echoes the input message back."""
 66      current_config = tool_config if tool_config is not None else {}
 67      prefix = current_config.get("prefix", "Echo: ")
 68  
 69      result = f"{prefix}{message}"
 70      log.info(f"[EchoTool] Echoing: {result}")
 71  
 72      return {
 73          "status": "success",
 74          "message": result,
 75      }
 76  ```
 77  
 78  Every tool function must be `async`. The `tool_context` and `tool_config` parameters are always the last two---SAM injects them automatically at runtime. All other parameters become the tool's input schema that the LLM sees and populates. In this example the LLM will see a single `message` string parameter. The `google.adk.tools` package is already included in the SAM Enterprise base image, so you do not need to install it separately.
 79  
 80  ## Step 3: Write the Agent YAML
 81  
 82  Create `custom-echo-agent.yaml` to define the agent. This file is passed to Helm at deploy time via `--set-file config.yaml=custom-echo-agent.yaml`. All `${...}` variables are resolved at runtime from environment variables that the Helm chart injects---you do not need to hardcode any values:
 83  
 84  ```yaml
 85  log:
 86    stdout_log_level: INFO
 87    log_file_level: DEBUG
 88    log_file: custom-echo-agent.log
 89  
 90  shared_config:
 91    - broker_connection: &broker_connection
 92        dev_mode: ${SOLACE_DEV_MODE, false}
 93        broker_url: ${SOLACE_BROKER_URL, ws://localhost:8080}
 94        broker_username: ${SOLACE_BROKER_USERNAME, default}
 95        broker_password: ${SOLACE_BROKER_PASSWORD, default}
 96        broker_vpn: ${SOLACE_BROKER_VPN, default}
 97        temporary_queue: ${USE_TEMPORARY_QUEUES, true}
 98  
 99    - models:
100      general: &general_model
101        model: ${LLM_SERVICE_GENERAL_MODEL_NAME}
102        api_base: ${LLM_SERVICE_ENDPOINT}
103        api_key: ${LLM_SERVICE_API_KEY}
104  
105    - services:
106      session_service: &default_session_service
107        type: "memory"
108        default_behavior: "PERSISTENT"
109  
110      artifact_service: &default_artifact_service
111        type: "memory"
112  
113  apps:
114    - name: custom-echo-agent-app
115      app_base_path: .
116      app_module: solace_agent_mesh.agent.sac.app
117      broker:
118        <<: *broker_connection
119  
120      app_config:
121        namespace: ${NAMESPACE}
122        supports_streaming: true
123        agent_name: "CustomEchoAgent"
124        display_name: "Custom Echo Agent"
125        model: *general_model
126        model_provider: 
127          - "general"
128  
129        instruction: |
130          You are a custom echo agent. When a user sends you a message,
131          use the echo_tool to echo it back to them.
132  
133        tools:
134          - tool_type: python
135            component_module: custom_echo_agent.tools
136            function_name: echo_tool
137            tool_config:
138              prefix: "Echo: "
139  
140        session_service: *default_session_service
141        artifact_service: *default_artifact_service
142  
143        agent_card:
144          description: "A custom agent that echoes messages back"
145          defaultInputModes: ["text"]
146          defaultOutputModes: ["text"]
147          skills:
148            - id: "echo_tool"
149              name: "Echo Tool"
150              description: "Echoes the input message back to the user"
151  
152        agent_card_publishing: { interval_seconds: 10 }
153        agent_discovery: { enabled: false }
154        inter_agent_communication:
155          allow_list: []
156          request_timeout_seconds: 30
157  ```
158  
159  The `component_module` field points to the Python module path of your tool (`custom_echo_agent.tools`), and `function_name` identifies which function to load. The `tool_config` section provides configuration values that SAM passes to your function's `tool_config` parameter at runtime.
160  
161  ## Step 4: Build the Docker Image
162  
163  Your project directory should look like this before building:
164  
165  ```
166  your-project/
167  ├── Dockerfile
168  ├── custom-echo-agent.yaml
169  └── custom-echo-agent/
170      ├── pyproject.toml
171      └── src/custom_echo_agent/
172          ├── __init__.py
173          └── tools.py
174  ```
175  
176  Create the following `Dockerfile`. It extends the SAM Enterprise base image, builds your plugin as a wheel, installs it, and fixes filesystem ownership so the runtime user can write to the SAM data directory:
177  
178  ```dockerfile
179  FROM <your-registry>/solace-agent-mesh-enterprise:<your-version>
180  
181  USER 0
182  WORKDIR /app
183  
184  # Install the build package (not included in the runtime image)
185  RUN pip install build
186  
187  # Copy and build the custom plugin
188  COPY custom-echo-agent/ /tmp/custom-echo-agent/
189  RUN sam plugin build /tmp/custom-echo-agent && \
190      pip install /tmp/custom-echo-agent/dist/*.whl && \
191      rm -rf /tmp/custom-echo-agent
192  
193  # Fix ownership: sam plugin build creates /app/.sam as root,
194  # but the runtime user (solaceai) needs write access
195  RUN chown -R solaceai:solaceai /app/.sam
196  
197  USER solaceai
198  
199  ENV SAM_CLI_HOME=/app/.sam
200  ENTRYPOINT ["solace-agent-mesh"]
201  CMD ["run", "/preset/agents"]
202  ```
203  
204  The image temporarily switches to `USER 0` (root) to install build tools and compile the plugin. The `sam plugin build` command produces a wheel under the plugin's `dist/` directory, which `pip install` then installs into the image's Python environment. After cleanup, `chown` ensures the SAM data directory is writable by the non-root `solaceai` user that runs at runtime.
205  
206  :::info
207  The `pip install build` step requires internet access during image build. This is the same build pattern used in the SAM Enterprise Dockerfile itself. In air-gapped environments, pre-download the `build` package and COPY it into the image.
208  :::
209  
210  ## Step 5: Push to Registry
211  
212  Build and push the image using Docker or Podman:
213  
214  ```bash
215  docker build -t <your-registry>/custom-echo-agent:1.0.0 .
216  docker push <your-registry>/custom-echo-agent:1.0.0
217  ```
218  
219  ```bash
220  podman build . -t <your-registry>/custom-echo-agent:1.0.0
221  podman push <your-registry>/custom-echo-agent:1.0.0
222  ```
223  
224  Tag images with a version number rather than relying on `latest`. This makes rollbacks straightforward and ensures Kubernetes pulls the exact image you intend.
225  
226  ## Adapting for Your Own Agent
227  
228  | What to change | Echo example | Your agent |
229  |----------------|-------------|------------|
230  | Plugin directory | `custom-echo-agent/` | `my-agent/` |
231  | Python package | `custom_echo_agent` | `my_agent` (underscores) |
232  | `component_module` in YAML | `custom_echo_agent.tools` | `my_agent.tools` |
233  | `agent_name` in YAML | `CustomEchoAgent` | `MyAgent` (PascalCase) |
234  | Helm release name | `custom-echo-agent` | `my-agent` (kebab-case) |
235  
236  To expose multiple tools from a single agent, add additional entries under `tools` and match them in `agent_card.skills`:
237  
238  ```yaml
239  tools:
240    - tool_type: python
241      component_module: my_agent.tools
242      function_name: first_tool
243  
244    - tool_type: python
245      component_module: my_agent.tools
246      function_name: second_tool
247  ```
248  
249  If your tool has third-party dependencies, add them to the `dependencies` list in `pyproject.toml`. In air-gapped environments the dependencies must already be present in the base image or copied into the build as local `.whl` files.
250  
251  ## Troubleshooting
252  
253  :::warning
254  The `sam plugin build` command creates `/app/.sam` as root. If you forget the `RUN chown -R solaceai:solaceai /app/.sam` line in your Dockerfile, the container will fail at runtime with a `PermissionError` because the `solaceai` user cannot write to that directory.
255  :::
256  
257  If you see `ModuleNotFoundError: No module named 'custom_echo_agent'` (or your package name) at runtime, the plugin was not installed correctly during the image build. Verify the install by running `docker run --rm <your-image> pip list | grep custom` against your built image. Check that the `packages` and `src-path` settings in `pyproject.toml` match your directory layout.
258  
259  ## Next Steps
260  
261  - Deploy your image to Kubernetes using the [Agent and Workflow Deployment](https://solaceproducts.github.io/solace-agent-mesh-helm-quickstart/docs/standalone-agent-deployment) guide
262  - Learn more about tool development patterns in [Creating Python Tools](../creating-python-tools.md)
263  - Explore plugin packaging in detail at [Plugins](../../components/plugins.md)