KOHAMI Docs
Getting started

Quickstart

Ce guide vous mène du login d'un senior jusqu'à la réception du premier event widget (météo, news, quiz) publié par le back-end sur le canal DataChannel LiveKit, en cinq étapes.

Prérequis

  • Bun 1.3+ installé localement
  • Accès à l'API dev (URL fournie par Bruno, https://<api-dev-domain>)
  • Token Scaleway dev si vous souhaitez tester contre l'environnement déployé
  • Un client LiveKit (côté tablette @livekit/react-native, ailleurs livekit-client)

Étape 1, login senior et récupération du JWT

Le back-end expose un endpoint REST POST /v1/auth/login qui retourne une paire accessToken / refreshToken au format JWT.

quickstart-login.ts
type LoginResponse = {
  accessToken: string;
  refreshToken: string;
  expiresIn: number;
};

async function loginSenior(
  apiBaseUrl: string,
  email: string,
  password: string
): Promise<LoginResponse> {
  const response = await fetch(`${apiBaseUrl}/v1/auth/login`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email, password, role: "senior" }),
  });
  if (!response.ok) {
    throw new Error(`Login failed: HTTP ${response.status}`);
  }
  return (await response.json()) as LoginResponse;
}

L'accessToken est un JWT de courte durée (15 minutes par défaut). Le refreshToken (7 jours) sert à le renouveler (cf. Authentification).

Étape 2, récupérer un token d'accès LiveKit

Le back-end signe les tokens LiveKit côté serveur, la tablette ne stocke jamais l'API secret. L'endpoint POST /v1/livekit/token retourne un token JWT scope participant lié à une room dédiée au senior.

quickstart-livekit-token.ts
type LivekitTokenResponse = {
  token: string;
  url: string;       // wss://livekit-dev.195-154-239-8.sslip.io
  roomName: string;  // le senior_id, forcé côté serveur depuis le JWT
  identity: string;  // le senior_id également (le client ne choisit ni l'un ni l'autre)
};

async function fetchLivekitToken(
  apiBaseUrl: string,
  accessToken: string
): Promise<LivekitTokenResponse> {
  const response = await fetch(`${apiBaseUrl}/v1/livekit/token`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${accessToken}`,
    },
    body: JSON.stringify({}),
  });
  if (!response.ok) {
    throw new Error(`LiveKit token request failed: HTTP ${response.status}`);
  }
  return (await response.json()) as LivekitTokenResponse;
}

Le contrat REST complet est typé dans @kohami/shared :

import type {
  LivekitTokenRequest,
  LivekitTokenResponse,
} from "@kohami/shared";

Étape 3, joindre la room et souscrire au DataChannel

Côté tablette (React Native), utilisez @livekit/react-native. L'exemple ci-dessous est en livekit-client pour la clarté, l'API est identique.

quickstart-join.ts
import { Room, RoomEvent } from "livekit-client";

const room = new Room({
  adaptiveStream: true,
  dynacast: true,
});

await room.connect(livekitUrl, livekitToken);

room.on(RoomEvent.DataReceived, (payload, _participant, _kind, topic) => {
  const decoded = new TextDecoder().decode(payload);
  console.log(`[${topic}]`, decoded);
});

Le back-end publie sur trois familles de topics (voir Realtime voice et Data channels pour la liste complète) :

  • kohami.widget.data, events outbound (résultats météo, news, quiz, vision, assistance)
  • kohami.user.*, events inbound (actions tablette, tap card, alert button)
  • kohami.agent.events, état du tour de parole (turn-state, end-of-turn)

Étape 4, parser le premier event widget

Tous les events DataChannel sont des objets JSON portant un champ discriminant kind. Importez les types et schémas Zod depuis @kohami/shared pour valider la trame côté client.

quickstart-parse-event.ts
import {
  NewsResultEventSchema,
  QuizQuestionEventSchema,
  WeatherResultEventSchema,
  type NewsResultEvent,
  type QuizQuestionEvent,
  type WeatherResultEvent,
} from "@kohami/shared";

type WidgetEvent = WeatherResultEvent | NewsResultEvent | QuizQuestionEvent;

function parseWidgetEvent(raw: string): WidgetEvent | null {
  const json = JSON.parse(raw) as { kind?: string };
  switch (json.kind) {
    case "weather:result":
      return WeatherResultEventSchema.parse(json);
    case "news:result":
      return NewsResultEventSchema.parse(json);
    case "quiz:question":
      return QuizQuestionEventSchema.parse(json);
    default:
      return null;
  }
}

Étape 5, déclencher un widget côté senior

Le pipeline simple consiste à demander un widget météo via l'API REST de simulation (POST /v1/dev/widgets/weather/trigger, dev only). Le back-end publie ensuite weather:result puis kohami.tool.open sur kohami.widget.data. La tablette reçoit le résultat dans le DataReceived handler.

quickstart-trigger.ts
await fetch(`${apiBaseUrl}/v1/dev/widgets/weather/trigger`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${accessToken}`,
  },
  body: JSON.stringify({ city: "Paris" }),
});

En production, c'est l'agent vocal qui décide de publier ces events après un appel d'outil (getCurrentWeather, getLatestNews, etc.). Le contrat de payload reste identique.

Aller plus loin