Files
zeekay 23aa07692d Integration packages ship compiled output only [skip ci]
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.
2026-09-30 01:33:36 -07:00
..

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 prompt Input.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 on api.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

  1. In the Azure portal β†’ Create a resource β†’ Azure Bot. Choose a name and, for Type of App, Multi Tenant (or Single Tenant / MSI).
  2. Under Configuration, set the Messaging endpoint to your public URL: https://<your-host>/api/messages.
  3. 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.
  4. 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 β€” scopes personal, team, groupChat (1:1, channel, group chat).
  • composeExtensions β€” askHanzo (action, contexts message/compose/ commandBox, with the four quick actions as a choiceset) and composeHanzo (query, inserts an AI draft).
  • permissions β€” identity, messageTeamMembers.
  • validDomains β€” api.hanzo.ai, token.botframework.com.

Identity

Two supported models, both in src/identity.ts:

  1. 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.
  2. Per-user link β€” a Teams user presents a Hanzo IAM JWT (via Teams SSO); resolveFromToken validates it with @hanzo/iam and derives the Hanzo org from the sub (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.