Molecule OS
SDK

Building a chat widget

The SDK's MVP use case — a streaming, cited chat widget with thumbs up/down feedback, in ~40 lines. Framework-agnostic core plus a React example.

The first thing most teams build on the SDK is a chat widget: a text box that streams a grounded answer token-by-token, shows its sources, and lets the user rate it. Every piece the widget needs is on the client — answerStream for the stream, Citation[] on the terminal frame for sources, and feedback for the rating.

The loop

import { Molecule, type AnswerStreamEvent, type Turn } from "@molecule/sdk";

const molecule = new Molecule({
  apiKey: process.env.NEXT_PUBLIC_MOLECULE_KEY!,   // a PUBLIC key — see the note
  baseUrl: "https://brain.example.com",
});

const history: Turn[] = [];

async function ask(question: string, onToken: (t: string) => void) {
  history.push({ role: "user", content: question });
  let answer = "";
  let decisionId = "";

  for await (const ev of molecule.answerStream(question, {
    surface: "site_chatbot",
    channel: "chat",
    history,
  })) {
    if (ev.type === "token") {
      answer += ev.delta;
      onToken(ev.delta);                 // render incrementally
    } else if (ev.type === "done") {
      decisionId = ev.decisionId;        // keep it for feedback
      renderSources(ev.citations);
    } else if (ev.type === "error") {
      throw new Error(ev.error);
    }
  }

  history.push({ role: "assistant", content: answer });
  return decisionId;
}

// When the user clicks 👍 / 👎:
async function rate(decisionId: string, rating: "up" | "down") {
  await molecule.feedback({ decisionId, rating });
}

Ship a PUBLIC key in the browser

A key exposed in page JavaScript must be a public key. By construction it can only unlock the tenant's public namespace (end_customer audience), so a site visitor can never reach private knowledge — even though the key is visible. Never put an internal_agent key in a browser bundle.

React example

A minimal streaming chat component. useRef holds the mutable history and the current decisionId; state drives the render.

"use client";
import { useRef, useState } from "react";
import { Molecule, type Turn } from "@molecule/sdk";

const molecule = new Molecule({
  apiKey: process.env.NEXT_PUBLIC_MOLECULE_KEY!,
  baseUrl: process.env.NEXT_PUBLIC_BRAIN_URL!,
});

export function Chat() {
  const [messages, setMessages] = useState<{ role: "user" | "assistant"; text: string }[]>([]);
  const [streaming, setStreaming] = useState(false);
  const history = useRef<Turn[]>([]);
  const lastDecision = useRef<string>("");
  const abort = useRef<AbortController | null>(null);

  async function send(q: string) {
    abort.current?.abort();                 // cancel any in-flight answer
    abort.current = new AbortController();
    setMessages((m) => [...m, { role: "user", text: q }, { role: "assistant", text: "" }]);
    history.current.push({ role: "user", content: q });
    setStreaming(true);

    let answer = "";
    try {
      for await (const ev of molecule.answerStream(q, {
        surface: "site_chatbot",
        channel: "chat",
        history: history.current,
        signal: abort.current.signal,
      })) {
        if (ev.type === "token") {
          answer += ev.delta;
          setMessages((m) => {
            const next = [...m];
            next[next.length - 1] = { role: "assistant", text: answer };
            return next;
          });
        } else if (ev.type === "done") {
          lastDecision.current = ev.decisionId;
        } else if (ev.type === "error") {
          answer = "Sorry — something went wrong.";
        }
      }
      history.current.push({ role: "assistant", content: answer });
    } finally {
      setStreaming(false);
    }
  }

  const rate = (rating: "up" | "down") =>
    lastDecision.current && molecule.feedback({ decisionId: lastDecision.current, rating });

  return (
    <div>
      {messages.map((m, i) => (
        <p key={i} data-role={m.role}>{m.text}</p>
      ))}
      {!streaming && lastDecision.current && (
        <div>
          <button onClick={() => rate("up")}>👍</button>
          <button onClick={() => rate("down")}>👎</button>
        </div>
      )}
      <input
        placeholder="Ask a question…"
        onKeyDown={(e) => {
          if (e.key === "Enter" && e.currentTarget.value.trim()) {
            send(e.currentTarget.value.trim());
            e.currentTarget.value = "";
          }
        }}
      />
    </div>
  );
}

Not ready to stream?

If you don't want to handle frames, answer() returns the whole composition in one call, and answerStreamToResponse() collects a stream into that same shape:

const res = await molecule.answer("what are your pricing plans?", {
  surface: "site_chatbot",
});
// res.answer, res.citations, res.decisionId

What the endpoints do

The widget rides on two endpoints added for exactly this use case:

On this page