Tutorial: translator app
In this tutorial, you will build a Custom App that translates messages into a target language by calling an upstream LLM deployed in DIAL. You will learn the key composition pattern: how one DIAL application calls another deployment (a model) through DIAL Core using per-request key propagation. No prior experience with DIAL app development beyond the Getting started tutorial is required.
Prerequisites
- Completed the Getting started with DIAL SDK tutorial
- Docker and Docker Compose (V2) installed
- At least 4 GB of free RAM (for running Ollama with a small model)
What you will build
A translator application that:
- Receives a user message through the Unified API.
- Calls a locally-running LLM (via Ollama) through DIAL Core to translate the message.
- Returns the translated text.
User → DIAL Core → Translator App → DIAL Core → LLM (Ollama) → DIAL Core → Translator App → DIAL Core → User
The critical concept in this tutorial is per-request key propagation — the mechanism that lets your app call other DIAL deployments securely without managing API keys manually.
Step 1: Create the project directory
mkdir -p dial-translator-app/translator
mkdir -p dial-translator-app/core
cd dial-translator-app
Final project layout:
dial-translator-app/
├── docker-compose.yml
├── core/
│ └── config.json
└── translator/
├── app.py
├── requirements.txt
└── Dockerfile
Step 2: Implement the translator app
Create translator/app.py:
import os
import json
import httpx
import uvicorn
from aidial_sdk import DIALApp
from aidial_sdk.chat_completion import ChatCompletion, Request, Response
DIAL_URL = os.environ.get("DIAL_URL", "http://core:8080")
MODEL_DEPLOYMENT = os.environ.get("MODEL_DEPLOYMENT", "chat-model")
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3.2:1b")
TARGET_LANGUAGE = os.environ.get("TARGET_LANGUAGE", "French")
class TranslatorApplication(ChatCompletion):
async def chat_completion(
self, request: Request, response: Response
) -> None:
last_message = request.messages[-1]
user_text = last_message.content or ""
translated_text = await self._ask_model(request, user_text)
with response.create_single_choice() as choice:
choice.append_content(translated_text)
async def _ask_model(
self, request: Request, question: str
) -> str:
"""Call a language model through DIAL Core."""
url = (
f"{DIAL_URL}/openai/deployments/"
f"{MODEL_DEPLOYMENT}/chat/completions"
)
headers = {
"Content-Type": "application/json",
}
payload = {
"model": OLLAMA_MODEL,
"messages": [
{
"role": "system",
"content": (
f"You are a translator. Translate the user's message "
f"into {TARGET_LANGUAGE}. Reply with only the "
f"translation, nothing else."
),
},
{
"role": "user",
"content": question
},
],
}
async with httpx.AsyncClient() as client:
resp = await client.post(
url, headers=headers, json=payload, timeout=60.0
)
resp.raise_for_status()
data = resp.json()
return data["choices"][0]["message"]["content"]
app = DIALApp(dial_url=DIAL_URL, propagate_auth_headers=True)
app.add_chat_completion("translator", TranslatorApplication())
To run locally with uvicorn:
uvicorn app:app --host 0.0.0.0 --port 5000
Key points in this code:
DIALApp(dial_url=DIAL_URL, propagate_auth_headers=True)enables automatic per-request key propagation. When DIAL Core sends a request to your app, it includes a per-request API key in theapi-keyheader. The SDK captures this key and automatically injects it into any outgoing HTTP requests your app makes toDIAL_URL.- The actual per-request key is injected automatically by the SDK's header propagation middleware. You never need to manage API keys manually.
Step 3: Add dependencies
Create translator/requirements.txt:
aidial-sdk[httpx]==0.27.0
uvicorn==0.34.0
The [httpx] extra enables HTTPX support in the SDK, which is needed for header propagation to work with the OpenAI Python SDK (it uses HTTPX internally).
Step 4: Create the Dockerfile
Create translator/Dockerfile:
FROM python:3.11-alpine
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 5000
CMD ["python", "app.py"]
Step 5: Configure DIAL Core
Create core/config.json. This configuration registers both the upstream model (Ollama) and your translator application:
{
"models": {
"chat-model": {
"type": "chat",
"displayName": "Local LLM (Ollama)",
"endpoint": "http://ollama:11434/v1/chat/completions"
}
},
"applications": {
"translator": {
"displayName": "Translator",
"description": "Translates messages into French using a local LLM",
"endpoint": "http://translator:5000/openai/deployments/translator/chat/completions"
}
},
"keys": {
"dial_api_key": {
"project": "TEST-PROJECT",
"role": "default"
}
},
"roles": {
"default": {
"limits": {
"chat-model": {},
"translator": {}
}
}
}
}
The limits section grants the default role access to both the chat-model and the translator application. The translator app itself uses the per-request key (which carries the caller's role) to access chat-model through DIAL Core.
Step 6: Write the Docker Compose file
Create docker-compose.yml:
services:
translator:
build: translator
depends_on:
- core
- ollama
environment:
DIAL_URL: "http://core:8080"
OLLAMA_MODEL: "llama3.2:1b"
MODEL_DEPLOYMENT: "chat-model"
TARGET_LANGUAGE: "French"
ollama:
image: ollama/ollama:0.3.10
ports:
- "11434:11434"
volumes:
- ./.ollama:/root/.ollama
# ========= DIAL =========
core:
image: epam/ai-dial-core:0.25.1
platform: linux/amd64
environment:
aidial.config.files: '["/opt/config/config.json"]'
aidial.storage.overrides: '{ "jclouds.filesystem.basedir": "data" }'
aidial.redis.singleServerConfig.address: "redis://redis:6379"
aidial.identityProviders.test.rolePath: 'roles'
aidial.identityProviders.test.disableJwtVerification: 'true'
aidial.encryption.secret: 'salt'
aidial.encryption.key: 'password'
ports:
- "8080:8080"
volumes:
- ./core:/opt/config
depends_on:
- redis
themes:
image: epam/ai-dial-chat-themes:0.10.0
platform: linux/amd64
chat:
image: epam/ai-dial-chat:0.26.0
platform: linux/amd64
ports:
- "3000:3000"
depends_on:
- themes
- core
environment:
NEXTAUTH_SECRET: "secret"
THEMES_CONFIG_HOST: "http://themes:8080"
DIAL_API_HOST: "http://core:8080"
DIAL_API_KEY: "dial_api_key"
ENABLED_FEATURES: "conversations-section,prompts-section,top-settings,top-clear-conversation,top-chat-info,top-chat-model-settings,empty-chat-settings,header,footer,request-api-key,report-an-issue,likes,conversations-sharing,input-files,attachments-manager,custom-applications,marketplace"
redis:
image: redis:7.2.4-alpine3.19
command: >
redis-server
--maxmemory 256mb
--maxmemory-policy volatile-lfu
--save ""
--appendonly no
Step 7: Pull a model into Ollama
Start the stack:
docker compose up --build -d
Wait for all containers to start, then pull a small model into Ollama:
docker compose exec ollama ollama pull llama3.2:1b
This downloads a 1B parameter model (approximately 1.3 GB). Wait for the download to complete.
Verify: Confirm the model responds:
docker compose exec ollama ollama run llama3.2:1b "Say hello"
You should see a text response from the model.
Step 8: Test the translator in DIAL Chat
- Open http://localhost:3000 in your browser.
- Open the Catalog and select Translator.
- Type a message — for example,
Hello, how are you today?— and send it.
Verify: The app responds with a French translation, such as Bonjour, comment allez-vous aujourd'hui?.
Step 9: Test via the API
curl http://localhost:8080/openai/deployments/translator/chat/completions \
-H "Content-Type: application/json" \
-H "api-key: dial_api_key" \
-d '{
"messages": [
{"role": "user", "content": "The weather is beautiful today."}
]
}'
Verify: The response contains a French translation in choices[0].message.content.
Step 10: Experiment with configuration
Change the target language by updating the environment variable and restarting the translator:
docker compose up -d --build translator
Or modify the TARGET_LANGUAGE value in docker-compose.yml to "Spanish", "German", or any other language, then restart.
Step 11: Stop the stack
docker compose down
What you learned
- How to call an upstream model from a Custom App using the OpenAI Python SDK pointed at DIAL Core.
- How per-request key propagation works —
DIALApp(dial_url=..., propagate_auth_headers=True)automatically injects authentication headers into outgoing requests. - How to configure DIAL Core with both a model and an application that depends on it.
- The app composition pattern: applications can call models (and other applications) through DIAL Core, inheriting platform governance (rate limits, logging, access control) on every hop.
Video walkthrough
A video version of this tutorial is available: Develop Translator App. The written tutorial above is the authoritative reference — the video may show older UI or versions.
Further reading
- Getting started with DIAL SDK — build and run a first Custom App with DIAL SDK
- Tutorial: build a RAG app — build an app that processes file attachments and retrieves context
- Tutorial: custom buttons app — build an app with interactive buttons in DIAL Chat
- Deploy a Custom App — take your app to production with Helm