A serverless Telegram bot that answers with multiple AI engines, generates images and translates text. Built on AWS with the AWS CDK: Lambda functions running container images, SNS/SQS for asynchronous messaging, DynamoDB for state and S3 for storage.
- AI chat: Gemini (Google), plus Qwen 3.5 and Llama 4 via Ollama Cloud — enable several engines and they answer your message in parallel
- Images: Ideogram (native text/typography rendering)
- Translation: DeepL (interactive multi-language flow)
- Voice: voice messages are transcribed with Amazon Transcribe and sent to your engines
- Deployment: two-stage (dev → prod) via CDK CLI or GitHub Actions (OIDC)
- Bot commands
- Engines
- Architecture
- Repository layout
- Local development
- Deployment (dev / prod)
- Post-deploy checks & troubleshooting
- Configuration reference
- Tests
Reply formatting uses Telegram MarkdownV2. Replies longer than one message are split into
parts labelled i of n. In groups, the bot only reacts when it is addressed
(@<bot_username> in the message/caption).
| Command | Description |
|---|---|
/start |
Welcome message and list of supported commands |
/help <command> |
Help for one command, e.g. /help tr, /help engines, /help imagine |
/engines |
Show the currently active engines |
/engines <list> |
Activate engines for parallel answering, comma separated — /engines gemini,llama,qwen. Persists in your user configuration |
/gemini · /llama · /qwen |
Shortcuts to switch to a single engine |
/reset |
Reset the conversation/context of the currently active engines |
/creative · /balanced · /precise |
Set the tone of responses (stored in your user configuration) |
/imagine <prompt> · /ideogram <prompt> |
Generate an image with Ideogram — e.g. /imagine cute kitty plays with a yarn ball |
/tr |
Interactive translation: pick target language(s), then send the text |
/tr <langs> |
One-shot translation — /tr pl,ru then send the text (DeepL codes, comma separated) |
/cancel |
Cancel an active /tr conversation |
Non-command input
- Plain text → sent to your active engines.
- Voice messages → transcribed, the transcript is shown to you and sent to your engines.
- Photos / documents / attachments → uploaded to S3 and fanned out to your active engines (engine-level file handling varies by provider).
| Command | Description |
|---|---|
/ping |
Health check — replies pong from each active engine |
/errors |
Tail CloudWatch Logs errors (last 3h) from the Lambda log groups |
/redrive |
Move messages stuck in the SQS DLQs back to their SNS topics |
⚠️ Admin limitation:TELEGRAM_BOT_ADMINScurrently supports one user id — see Configuration reference.
| Engine | Provider | Purpose | Handler |
|---|---|---|---|
gemini |
Google Gemini 3 (gemini-3.8-flash, GA) |
Text chat | engines/gemini.py |
qwen |
Alibaba Qwen 3.5 (Ollama Cloud, qwen3.5:cloud) |
Text chat (sync) | engines/ollama.py |
llama |
Meta Llama 4 (Ollama Cloud, llama4:maverick) |
Text chat (sync) | engines/ollama.py |
| Ideogram | ideogram.ai | Image generation (/imagine, /ideogram), async via polled queue |
engines/ideogram_img.py, engines/ideogram_result.py |
| DeepL | DeepL API | Translation (/tr) |
engines/deepl_tr.py |
The qwen and llama engines share one OpenAI-compatible Ollama Cloud handler; CDK sets the
model tag per Lambda via the OLLAMA_MODEL/OLLAMA_ENGINE environment variables
(override the tags to anything your Ollama account exposes under
https://ollama.com/search?c=cloud).
Engine handlers expose an SNS sns_handler (or sqs_handler/callback_handler) and are
deployed as separate Lambda functions; see stacks/engines_stack.py.
Three CDK stacks (app.py):
- EnginesStack — SNS request topic, per-engine Python Lambda functions (DLQ + alarms) and the Ideogram polling queue/result handler.
- DatabaseStack — DynamoDB tables:
user-configurations,user-conversations,user-context(TTL 60 d),request-jobs(TTL 10 d). - ChatBotStack — Telegram bot Lambda exposed via a Function URL (webhook target), result-processing Lambda, webhook trigger, S3 bucket, alarms. Depends on the two stacks above.
Request flow:
Telegram webhook ──► BotHandler (Function URL)
│ publish (SNS request-ai-topic, filter attributes: type / engines)
▼
┌─────────────── engine Lambdas (Gemini, LLama, Qwen, Ideogram, DeepL) ─────────┐
│ Ideogram: result polled via SQS delayed queue; LLama: result via webhook │
└───────────────► publish (SNS result-ai-topic) ◄───────────────────────────────┘
▼
ResultProcessingHandler ──► Telegram replies
Configuration is read from SSM Parameter Store at Lambda cold start and state lives in
DynamoDB. All Lambda functions are plain Python 3.14 ZIP functions (Runtime.PYTHON_3_14);
each project's code and locked dependencies are bundled by scripts/build_bundles.py with uv
(no Docker, no ECR). The entry point is chosen per function via the CDK handler
(e.g. lambda.chatbot.telegram_api_handler).
app.py CDK entry point (defines the 3 stacks)
stacks/ CDK stack definitions (chatbot, engines, database)
lambda/ Telegram bot Lambdas: chatbot, results, webhook, utils, help
engines/ AI engine handlers: gemini, ollama (llama + qwen), ideogram, deepl + shared utils
tests/ pytest tests (live tests skipped without AWS)
scripts/build_bundles.py Bundles lambda/ & engines/ code + locked deps into ZIPs (uv, no Docker)
pyproject.toml Root project: dev/CDK deps + lambda/engines dependency groups
UPDATE_VARS.md AWS configuration checklist (SSM params, S3 auth files, secrets)
docs/MARKDOWN_UPGRADE_PLAN.md Plan for Telegram Rich Messages / formatting migration
Each of lambda/ and engines/ is an independent uv project with its own pyproject.toml
and uv.lock; the root project aggregates them as dependency groups for local dev.
The IaC is Python CDK: the library (aws-cdk-lib, pinned in pyproject.toml) is a Python
package installed by uv; the cdk CLI is a Node-distributed
program pinned as a project-local devDependency in package.json (run via npx aws-cdk). Node
is only the CLI's runtime — no application code depends on it.
Requirements: Python 3.14+ with uv, Node.js 20+ (for the pinned CLI), AWS credentials. No Docker is needed anywhere — Lambda bundles are produced by uv.
uv sync --all-groups # install dev/CDK + lambda + engines deps into .venv
npm ci # install the pinned aws-cdk CLI into ./node_modules
python scripts/build_bundles.py # build lambda/ & engines/ ZIP bundles (uv, no Docker)
uv run pytest # unit tests (AWS/live tests are @pytest.mark.skip)Invoke CDK as
npx aws-cdk …. Do notnpm install -g aws-cdk, and do not rely on a barepythonfrom outside the activated venv —npx aws-cdkrunspython app.py, so Python must resolve to the project venv.
Keep lockfiles in sync after editing pyproject.toml:
uv lock # root project (dev/CDK tooling)
(cd lambda && uv lock) # bot Lambda deps
(cd engines && uv lock) # engine depsThe project supports two deployment stages, dev and prod. Both are deployed with the
same CDK code; the STAGE value is only used in a few resource names (e.g. the S3 bucket
chatbotstack-s3-bucket-<stage>-tmp).
| dev | prod | |
|---|---|---|
| Purpose | Sandbox / continuous integration | Stable, user-facing |
| Git branch (CI trigger) | development (push) |
main (push) or manual |
| AWS account | Separate dev account (recommended) | Production account |
| Region | Any (e.g. eu-central-1) |
Any |
STAGE |
dev |
prod |
| GitHub environment | dev |
prod |
⚠️ Accounts and regions — read this before deploying Most resource names are not stage-scoped: DynamoDB tables, SNS topics, SQS queues, Lambda function names/log groups and the SSM parameters are fixed. Therefore dev and prod cannot run in the same account+region at the same time — name collisions and clobbered SSM values. Either give each stage its own AWS account (recommended), or run them in different regions of one account.
Before the first deployment to an account/region, complete the checklist in
UPDATE_VARS.md. In short:
- Create the manual SSM parameters (
TELEGRAM_TOKEN,TELEGRAM_BOT_ADMINS,SECRET_TOKEN,ALARM_EMAIL,GEMINI_API_KEY,DEEPL_AUTHKEY,OLLAMA_API_KEY,IDEOGRAM_USER) — all asType=String(see the caveat inUPDATE_VARS.md). - Seed the S3 auth file
google_auth.json(Ideogram refresh token) — the bucket is created by ChatBotStack, so upload the file right after the first deploy and before the first/imagine. - Confirm the SNS alarm e-mail subscription (one click in the inbox).
- Build the Lambda bundles:
python scripts/build_bundles.py(see "Local development"). No Docker is required anywhere — bundles are built with uv, and deploys run from CI or any machine with uv.
Each (account, region) pair must be bootstrapped once before the first CDK deploy:
npx aws-cdk bootstrap aws://<ACCOUNT_ID>/<REGION>
⚠️ The bootstrap ("CDKToolkit") stack version is tied to the CDK CLI it was created with, not to the code. After upgradingaws-cdk-lib— this repo now pins 2.268.x, which requires bootstrap v30+ — re-run the same bootstrap command before the next deploy. Otherwisenpx aws-cdk deployaborts with "Bootstrap toolkit stack version 30 or later is needed; current version: 27" and, because the deploy role's permissions come from the toolkit stack, it may also lackcloudformation:DescribeEvents(only granted by the newer bootstrap template) →AccessDeniedwhile reporting change-set validation failures. Bootstrapping is idempotent: re-running it with a current CLI updates the toolkit stack and its IAM roles in place. It needs CloudFormation + IAM rights on theCDKToolkitstack, so run it with the account owner/administrator.
# 1. Configure the target stage
export CDK_ACCOUNT=<ACCOUNT_ID>
export CDK_REGION=<REGION>
export STAGE=dev # or prod
# 2. Install the pinned CDK CLI (project-local), Python deps, and Lambda bundles
npm ci
uv sync --all-groups
python scripts/build_bundles.py
# 3. Sanity-check (list stacks)
npx aws-cdk ls # EnginesStack DatabaseStack ChatBotStack
# 4. Review what will change
npx aws-cdk diff
# 5. Deploy everything (order/dependencies are resolved automatically)
npx aws-cdk deploy --all --require-approval never
# 6. Deploy a single stack if needed (respects dependencies)
npx aws-cdk deploy ChatBotStack
# 7. Verify the stage (see "Post-deploy checks" below)Notes:
npx aws-cdk deploy --allruns EnginesStack, DatabaseStack, ChatBotStack in dependency order; ChatBotStack's webhook trigger registers the Telegram webhook at the end of the deploy.- No Docker required:
python scripts/build_bundles.pyproduces the ZIP bundles with uv before deploying; the bundles are uploaded as plain S3 assets. - If you change only Lambda code, redeploy ChatBotStack and/or EnginesStack (the affected bundle is rebuilt and its asset re-uploaded).
Two workflows are provided (.github/workflows/deploy-dev.yml, deploy-prod.yml). They use
OpenID Connect and never store long-lived AWS keys:
| Workflow | Trigger | STAGE |
GitHub environment |
|---|---|---|---|
deploy-dev.yml |
push to development, or workflow_dispatch |
dev |
dev |
deploy-prod.yml |
push to main, or workflow_dispatch |
prod |
prod |
Pipeline steps (both files): checkout → assume AWS role (OIDC) → setup Node 22 + Python 3.14 →
npm ci (pinned CDK CLI) → uv sync --all-groups → python scripts/build_bundles.py →
npx aws-cdk deploy --all --require-approval never. Bootstrapping is not
part of the pipeline: a least-privilege AWS_ROLE cannot manage the CDKToolkit stack (a
bootstrap step fails with AccessDenied on cloudformation:DescribeStacks), so bootstrap each
(account, region) manually with an administrator — see "One-time bootstrap" — before the first
run and after every aws-cdk-lib upgrade.
Required repository/environment secrets:
| Secret | Meaning |
|---|---|
AWS_ROLE |
ARN of the IAM role the workflow assumes (trusted by the GitHub OIDC provider) |
AWS_REGION |
Region the workflow deploys to |
Per environment: create a dev and a prod environment in Settings → Environments, each
with its own AWS_ROLE/AWS_REGION, pointing at its dedicated AWS account. Create the SSM
parameters listed in UPDATE_VARS.md in each account before the first
workflow run (the workflows do not provision secrets).
The workflows do not bootstrap: a deploy-only
AWS_ROLE(typical least-privilege CDK setup) cannot manage theCDKToolkitstack — a bootstrap step run as that role fails withAccessDeniedoncloudformation:DescribeStacks. Instead, runnpx aws-cdk bootstrap aws://<ACCOUNT>/<REGION>once manually with an administrator, and re-run it after everyaws-cdk-libupgrade that raises the required bootstrap version (the deploy fails with a "Bootstrap toolkit stack version … is needed" error until you do).
npx aws-cdk diff # inspect
npx aws-cdk destroy --all # tears down stacks (see note below)RemovalPolicy note: the S3 bucket, queues and log groups are destroyed with the stack,
but the DynamoDB tables are RETAINed and remain after npx aws-cdk destroy (delete them
manually if you really want them gone).
- Webhook registered:
curl https://api.telegram.org/bot<TOKEN>/getWebhookInfoshould show theBOT_LAMBDA_URLand yourSECRET_TOKEN. - Alarm e-mail: confirm the SNS subscription sent to
ALARM_EMAIL. - Bot answers: send
/start, then/ping(admin) — each active engine should replypong. - Images: send
/imagine <prompt>— if it fails, checkgoogle_auth.jsonin the bot bucket (BOT_S3_BUCKET) andIDEOGRAM_USERconsistency. - Secrets rotation: SSM values are read at Lambda cold start; after rotating a value, update SSM and redeploy so warm instances pick it up.
- Lambda crashes at cold start with SSM errors: a parameter from
UPDATE_VARS.mdis missing, or was created asSecureString(must beString). - Deploy aborts:
AWS::Logs::LogGroup… "already exists" (early validation) — the stacks declare explicit log groups (/aws/lambda/<Handler>, 2-week retention), but those groups already exist as Lambda-created resources that CloudFormation does not own (left over from a stack version that did not declare them). CFN cannot adopt them through a normal update, so this is a one-time per-account fix: delete the stale groups — only the ones the deploy error lists — and redeploy (their old logs are lost; CloudWatch recreates the groups on the next function run, and after this deploy CFN owns them for good):Run against the same account+region as the deploy, and repeat for each stage account before its first deploy of this code. Handlers that never ran before (e.g.aws logs delete-log-group --log-group-name /aws/lambda/DeepLHandler aws logs delete-log-group --log-group-name /aws/lambda/GeminiHandler aws logs delete-log-group --log-group-name /aws/lambda/IdeogramHandler aws logs delete-log-group --log-group-name /aws/lambda/IdeogramResultHandler aws logs delete-log-group --log-group-name /aws/lambda/LLamaHandler aws logs delete-log-group --log-group-name /aws/lambda/BotHandler aws logs delete-log-group --log-group-name /aws/lambda/ResultProcessingHandler aws logs delete-log-group --log-group-name /aws/lambda/WebhookTriggerHandler npx aws-cdk deploy --all --require-approval never
QwenHandler) are not affected — their groups do not exist yet. - Deploy fails:
AWS::Lambda::Function… "does not have permission to access the provided code artifact" — while updating a container-image function, Lambda cannot pull the new image from ECR at deploy time. Known causes: the ECR asset-repository resource policy for the Lambda service is missing/stale (e.g. after anpx aws-cdk bootstraprun, see aws/aws-cdk#18473), or an ordering race in a large change set. First just retry the deploy — the race usually does not reproduce. If it persists: both stacks now grant their Lambda execution rolesecr:GetAuthorizationToken/BatchCheckLayerAvailability/BatchGetImage/GetDownloadUrlForLayer(role-based image retrieval, robust regardless of repository policy); alternatively repair the repository policy of the specific repo the function points at (find it withaws lambda get-function --function-name IdeogramHandler --query 'Code.ImageUri', thenaws ecr set-repository-policy).
Every runtime parameter, S3 auth/cookie file, deploy variable and CI secret — with exact
names, where each is read, example values and aws CLI snippets — lives in
UPDATE_VARS.md. Use it as the checklist for every new stage/account.
uv run pytestUnit tests live in tests/. Tests that require live AWS resources or external APIs are
marked @pytest.mark.skip and only run against a fully configured account.