Skip to content

Turn feedback

kaboo-react ships a small, storage-free feedback mechanism for scoring turns: a headless useTurnFeedback hook plus an optional TurnFeedback thumbs up/down component. The library resolves which turn is being scored (its traceId / runId / turnId) and hands the payload to a host callback — where the score goes is entirely the host's decision, consistent with the slots philosophy.

The typical pipeline: the workflow service runs with telemetry enabled, every turn's trace id rides the activity stream into StreamGroup.traceId and ActivityState.traceByRun, and the host forwards feedback to its observability backend's score API (e.g. Langfuse create_score) keyed by that trace id. See the kaboo-workflows evaluation guide for the service side.

Drop-in component

TurnFeedback is designed for the turnFooter slot, so feedback renders under each completed turn:

import { KabooProvider, TurnFeedback, type TurnFooterContext } from "@pgege/kaboo-react";

const slots = {
  turnFooter: (ctx: TurnFooterContext) => (
    <TurnFeedback
      groups={ctx.groups}
      onFeedback={(p) =>
        fetch("/api/feedback", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(p),
        })
      }
    />
  ),
};

<KabooProvider runtimeUrl="/api/copilotkit" agent="kaboo" slots={slots}>
  …
</KabooProvider>;

Thumbs up submits immediately; thumbs down opens an optional "what went wrong?" comment box first. After submission the control collapses into a thank-you line for that turn.

Props

Prop Type Description
groups GroupEntry[] The turn's activity groups (e.g. TurnFooterContext.groups).
onFeedback (payload: FeedbackPayload) => void \| Promise<void> Host sink for the score. May be async; the buttons disable while it's in flight.
requireTrace boolean Render nothing when the turn has no resolvable traceId (service ran without telemetry). Default false: feedback is still collected with runId/turnId correlation only.
label string Prompt next to the buttons. Default "Was this helpful?".

Payload

interface FeedbackPayload {
  value: 0 | 1;        // 1 = thumbs up, 0 = thumbs down
  comment?: string;    // free text, negative feedback only by default
  traceId?: string;    // OTel trace id of the turn's root run (telemetry on)
  runId?: string;      // AG-UI run id
  turnId?: string;     // kaboo's logical turn id
}

Headless hook

Build any UI on top of useTurnFeedback — star ratings, inline surveys, keyboard shortcuts:

import { useTurnFeedback, type GroupEntry } from "@pgege/kaboo-react";

function Score({ groups }: { groups: GroupEntry[] }) {
  const { submit, submitted, submitting, traceId } = useTurnFeedback(
    groups,
    (p) => fetch("/api/feedback", { method: "POST", body: JSON.stringify(p) }),
  );
  if (submitted != null) return <span>Thanks!</span>;
  return (
    <button disabled={submitting} onClick={() => submit(1)}>
      Helpful{traceId ? "" : " (untraced)"}
    </button>
  );
}

The hook resolves the turn's ids with resolveTurnIds(groups, traceByRun) (also exported): the run-level trace from ActivityState.traceByRun wins over any group's own traceId, because nested agents may complete under their own invocation traces.

Host pattern

The host backend owns storage and authentication. A reference implementation forwarding to Langfuse:

```ts no-verify // POST /api/feedback (authenticated like any other app endpoint) import { Langfuse } from "langfuse";

const langfuse = new Langfuse(); // LANGFUSE_PUBLIC_KEY / SECRET_KEY / BASEURL

app.post("/api/feedback", async (req, res) => { const { traceId, value, comment, runId, turnId } = req.body; if (traceId) { langfuse.score({ traceId, name: "user-feedback", value, comment: comment || undefined, }); await langfuse.flushAsync(); } // else: persist {runId, turnId, value, comment} however you like res.sendStatus(204); }); ```

Scores land on the exact trace of the run the user judged, next to its token usage, cost, and tool spans — negative feedback becomes a review queue, and reviewed items graduate into your golden eval dataset.

Styling

The component uses kaboo-feedback* class names (kaboo-feedback, kaboo-feedback-label, kaboo-feedback-btn, kaboo-feedback-btn-active, kaboo-feedback-comment, kaboo-feedback-input, kaboo-feedback-done) and inherits the kaboo theme variables, so it follows your existing theming overrides.