canva, canvas-lms, cards, docusign, epic, imanage, procore, quickbooks, shopify, workday and teams publish dist without source maps or src. A watch build still writes maps for local debugging.
Hanzo AI for Microsoft Teams
A Bot Framework app that brings Hanzo AI into Microsoft Teams β the
comms standard for enterprises and regulated, non-technical orgs. DM Hanzo or
@mention it in a channel, run quick actions on any message ("Ask Hanzo"), or
compose an AI draft β all answered by Hanzo AI and rendered as native
Adaptive Cards.
It reuses the same backend and design as every other Hanzo surface: model
calls go through api.hanzo.ai / /v1 via the headless
@hanzo/ai client (never an /api/
path), identity is Hanzo IAM via @hanzo/iam,
and every card is built from the shared canonical panel spec in
@hanzo/cards β one PanelSpec β toAdaptiveCard. No new API
surface, no hand-written Adaptive Card JSON.
What it does
- Chat β a DM or
@Hanzo <prompt>in a channel runs a completion and replies with the Hanzo assistant panel (model picker, quick actions, prompt). - Quick actions β Draft reply, Summarize, Explain, Extract action items β as Adaptive Card buttons and as the "Ask Hanzo" message-action command on any message.
- Compose β the "Compose with Hanzo" command generates an AI draft to insert while writing a message.
- Adaptive Card actions β model picker (
Input.ChoiceSet), quick-action buttons, and a promptInput.Text+ submit, all from@hanzo/cards.
Architecture
Teams ββPOST /api/messagesβββΆ server.ts (restify + CloudAdapter)
β
βΌ
bot.ts (TeamsActivityHandler)
ββββββββββββββββββββββββββΌββββββββββββββββββββββββββ
βΌ βΌ βΌ
context.ts dispatch.ts panels.ts
(message/thread text) (data.action β intent) (PanelSpec β Adaptive Card
β β via @hanzo/cards)
βββββββββββββ¬βββββββββββββ
βΌ
hanzo.ts βββΆ @hanzo/ai βββΆ api.hanzo.ai /v1
Everything except server.ts (opens the socket) and bot.ts (wires botbuilder)
is a pure module β dispatch, panels, context, hanzo, identity,
config β so the routing, card assembly, and text extraction are unit-tested in
isolation.
The Action.Submit dispatch flow
Every button and input in a Hanzo card comes from @hanzo/cards, which stamps a
stable id onto each Action.Submit as data.action, and keys the inputs by
ActionId.ModelSelect (hanzo_model_select) and PROMPT_INPUT_ID
(hanzo_prompt). Teams returns that merged payload on submit, and
dispatch(data) turns it into one typed CardIntent:
data.action |
Also carries | β CardIntent |
|---|---|---|
hanzo_sign_in |
β | { kind: 'sign_in' } |
hanzo_model_select |
hanzo_model_select = model id |
{ kind: 'model_select', model } |
hanzo_prompt_submit |
hanzo_prompt, hanzo_model_select |
{ kind: 'prompt', prompt, model? } |
hanzo_quick_action:<chip> |
chip, hanzo_prompt, model |
{ kind: 'quick_action', chip, β¦ } |
| anything else | β | { kind: 'unknown', action? } |
bot.ts receives the submit two ways and routes both through the same
dispatch: a classic Action.Submit arrives as a message activity with
activity.value set (handled in onMessage), and an Action.Execute arrives
via onAdaptiveCardInvoke (invokeValue.action.data). One routing table, two
entry points.
Requirements
- Node 18+ and
pnpm. - An Azure Bot registration (app id + secret) β see below.
- A Hanzo API key (
sk-β¦) with model access onapi.hanzo.ai.
Configuration
All secrets come from the environment β nothing is committed. loadConfig
(src/config.ts) reads and validates them at startup:
| Variable | Required | Default | Meaning |
|---|---|---|---|
MICROSOFT_APP_ID |
yes | β | Azure Bot app id (botId). |
MICROSOFT_APP_PASSWORD |
yes | β | Azure Bot client secret. |
HANZO_API_KEY |
yes | β | Hanzo API key (bearer to api.hanzo.ai). |
MICROSOFT_APP_TYPE |
no | MultiTenant |
MultiTenant / SingleTenant / UserAssignedMSI. |
MICROSOFT_APP_TENANT_ID |
no | "" |
AAD tenant id (required for SingleTenant). |
HANZO_BASE_URL |
no | https://api.hanzo.ai |
Override the AI base URL (local dev). |
HANZO_MODELS |
no | zen5,zen5-flash,zen5-mini,zen5-coder,zen5-pro |
Comma-separated picker models. |
HANZO_DEFAULT_MODEL |
no | first of HANZO_MODELS |
Default model id. |
PORT |
no | 3978 |
HTTP port for the messaging endpoint. |
Develop
pnpm install # from the monorepo root (workspace), or standalone here
pnpm typecheck # tsc --noEmit
pnpm test # vitest β pure logic (dispatch, panels, context, config, β¦)
pnpm build # tsup β dist/ (library + runnable server)
pnpm start # node dist/server.js (needs the env above)
Run locally against Teams with a tunnel (e.g. devtunnel or ngrok) pointing
at http://localhost:3978/api/messages.
Azure Bot registration
- In the Azure portal β Create a resource β Azure Bot. Choose a name and, for Type of App, Multi Tenant (or Single Tenant / MSI).
- Under Configuration, set the Messaging endpoint to your public URL:
https://<your-host>/api/messages. - Under Configuration β Microsoft App ID, copy the App ID β
MICROSOFT_APP_ID. Click Manage β Certificates & secrets β New client secret β copy the value βMICROSOFT_APP_PASSWORD. - Under Channels, add the Microsoft Teams channel.
Build & sideload the Teams app package
The Teams app package is a zip of manifest.json + color.png + outline.png.
The build step substitutes ${{β¦}} placeholders from the environment and zips:
MICROSOFT_APP_ID=<your-bot-app-id> \
TEAMS_APP_ID=<a-guid-for-the-app> \ # optional; defaults to MICROSOFT_APP_ID
pnpm package # β dist/hanzo-teams.zip
Then upload it one of two ways:
- Developer Portal / Teams client β Teams β Apps β Manage your apps
β Upload an app β Upload a custom app β pick
dist/hanzo-teams.zip. - Teams Admin Center (org-wide) β Teams apps β Manage apps β Upload new app, then set the org/app permission policy so users can install it.
Manifest highlights (manifest/manifest.json)
- Schema
v1.16. botsβ scopespersonal,team,groupChat(1:1, channel, group chat).composeExtensionsβaskHanzo(action, contextsmessage/compose/commandBox, with the four quick actions as achoiceset) andcomposeHanzo(query, inserts an AI draft).permissionsβidentity,messageTeamMembers.validDomainsβapi.hanzo.ai,token.botframework.com.
Identity
Two supported models, both in src/identity.ts:
- Per-tenant key (default, complete out of the box) β the tenant is
provisioned one
HANZO_API_KEY; every Teams user acts as that Hanzo principal. The enterprise/regulated-org pattern. - Per-user link β a Teams user presents a Hanzo IAM JWT (via Teams SSO);
resolveFromTokenvalidates it with@hanzo/iamand derives the Hanzo org from thesub(org/username) claim.
Deploy
Build the image in CI (ghcr.io/hanzoai/teams, --platform linux/amd64) and
run it behind the platform ingress; set the Azure Bot Messaging endpoint to
https://<host>/api/messages. Secrets (MICROSOFT_APP_*, HANZO_API_KEY) come
from KMS, never from the image or the repo. GET /healthz is the readiness
probe.