mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
238 lines
9.2 KiB
TypeScript
238 lines
9.2 KiB
TypeScript
import { LLMTrace, LLMTraceEvent } from '~/queries/schema/schema-general'
|
|
|
|
import { normalizeMessage, normalizeMessages } from './messageNormalization'
|
|
import { messageSignature } from './messageSignature'
|
|
import { CompatMessage } from './types'
|
|
import { eventLabel, formatAiErrorForDisplay, readAiInput, readAiOutput } from './utils'
|
|
|
|
// Heuristic mirrors the AI observability skill script `print_summary.py` at
|
|
// `products/ai_observability/skills/exploring-llm-traces/scripts/` — keep in sync.
|
|
|
|
/**
|
|
* Pick the event that represents the user-visible turn for a trace.
|
|
*
|
|
* Current heuristic: the latest `$ai_generation` by `createdAt` — an
|
|
* implementation detail callers should treat as opaque. A future opt-in
|
|
* `$ai_user_visible: true` convention would change how this picks. Wrong
|
|
* for traces where the last generation is a logging/cleanup call; the
|
|
* "Show steps" affordance is the user's fallback.
|
|
*/
|
|
export function pickUserVisibleTurn(trace: LLMTrace | undefined): LLMTraceEvent | undefined {
|
|
if (!trace?.events?.length) {
|
|
return undefined
|
|
}
|
|
let latest: LLMTraceEvent | undefined
|
|
let latestTs = -Infinity
|
|
for (const event of trace.events) {
|
|
if (event.event !== '$ai_generation') {
|
|
continue
|
|
}
|
|
const ts = new Date(event.createdAt).getTime()
|
|
if (ts > latestTs) {
|
|
latest = event
|
|
latestTs = ts
|
|
}
|
|
}
|
|
return latest
|
|
}
|
|
|
|
/**
|
|
* Maps each normalized message back to its index in the raw `$ai_input`.
|
|
* Many-to-one: `normalizeMessage` may expand one raw entry into multiple
|
|
* messages. A leading `-1` marks the synthetic "available tools" message.
|
|
*
|
|
* Used by `ConversationMessagesDisplay` to map per-message sentiment to the
|
|
* right rendered bubble after dedup. Also used by the Trace page renderer.
|
|
*/
|
|
export function buildInputSourceIndices(rawInput: unknown, tools: unknown): number[] {
|
|
const indices: number[] = []
|
|
if (tools) {
|
|
indices.push(-1)
|
|
}
|
|
if (Array.isArray(rawInput)) {
|
|
for (let i = 0; i < rawInput.length; i++) {
|
|
const expanded = normalizeMessage(rawInput[i], 'user')
|
|
for (let j = 0; j < expanded.length; j++) {
|
|
indices.push(i)
|
|
}
|
|
}
|
|
}
|
|
return indices
|
|
}
|
|
|
|
export interface SessionTurnError {
|
|
/** Human-readable label for *what* failed — `$ai_span_name`, `$ai_model`, or the raw event type as a last resort. */
|
|
label: string
|
|
/** Extracted error message. For `$ai_error` objects we surface `.message` directly; everything else is JSON-stringified. */
|
|
message: string
|
|
}
|
|
|
|
export interface SessionTurn {
|
|
/** The trace this turn corresponds to. */
|
|
trace: LLMTrace
|
|
/** True if `fullTraces[trace.id]` was available and we computed messages. */
|
|
isLoaded: boolean
|
|
/** Input messages new to this turn (already-seen messages from prior turns hidden). */
|
|
newInputs: CompatMessage[]
|
|
/** Output messages from this turn's last generation. */
|
|
outputs: CompatMessage[]
|
|
/** The event sourced by `pickUserVisibleTurn` — used for model, error, latency, and the id passed to renderers. */
|
|
userVisibleTurn?: LLMTraceEvent
|
|
/**
|
|
* Distinct tool names called in this turn, in first-appearance order. Pulled
|
|
* from both `$ai_span_name` on instrumented `$ai_span` events and from
|
|
* `tool_calls` / `tool_use` parts in generation outputs.
|
|
*/
|
|
tools: string[]
|
|
/**
|
|
* Distinct errors in the trace, deduped by `label + message` and ordered by
|
|
* first chronological occurrence. Retries of the same failure collapse to a
|
|
* single entry; truly distinct failures all appear. `trace.errorCount` still
|
|
* reflects total error events (including retries); `errors.length` is the
|
|
* count of UNIQUE kinds.
|
|
*/
|
|
errors: SessionTurnError[]
|
|
}
|
|
|
|
// Returns the tool names invoked across events in chronological order (duplicates preserved)
|
|
export function getToolNamesCalled(events: LLMTraceEvent[]): string[] {
|
|
const sorted = [...events].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
|
const names: string[] = []
|
|
// Pulls from `$ai_span_name` on instrumented `$ai_span` events
|
|
// and from `tool_calls[].function.name` on normalized `$ai_generation` outputs
|
|
for (const event of sorted) {
|
|
if (event.event === '$ai_span') {
|
|
const spanName = event.properties.$ai_span_name
|
|
if (typeof spanName === 'string' && spanName) {
|
|
names.push(spanName)
|
|
}
|
|
} else if (event.event === '$ai_generation') {
|
|
for (const msg of normalizeMessages(event.properties.$ai_output_choices, 'assistant').messages) {
|
|
for (const tc of msg.tool_calls ?? []) {
|
|
const name = tc.function?.name
|
|
if (typeof name === 'string' && name) {
|
|
names.push(name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
function getToolNamesCalledUnique(events: LLMTraceEvent[]): string[] {
|
|
// `Set` insertion order preserves first-appearance
|
|
return Array.from(new Set(getToolNamesCalled(events)))
|
|
}
|
|
|
|
/**
|
|
* Distinct errors keyed by `label + message`, in chronological first-appearance
|
|
* order. Retries of the same failure collapse to one entry. The chip already
|
|
* shows the raw event count (including retries); listing *distinct kinds* of
|
|
* failures inline reads more usefully than listing every retry.
|
|
*/
|
|
function collectDistinctErrors(events: LLMTraceEvent[]): SessionTurnError[] {
|
|
const sorted = [...events].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
|
const seen = new Set<string>()
|
|
const ordered: SessionTurnError[] = []
|
|
for (const event of sorted) {
|
|
// A populated `$ai_error` payload is the authoritative signal — if it's
|
|
// there, treat the event as an error regardless of `$ai_is_error`.
|
|
// (Note that Insights SDKs serialize booleans as strings)
|
|
const hasErrorPayload = !!event.properties.$ai_error
|
|
const isErrorFlag = event.properties.$ai_is_error
|
|
const isError = hasErrorPayload || isErrorFlag === true || isErrorFlag === 'true'
|
|
if (!isError) {
|
|
continue
|
|
}
|
|
const label = eventLabel(event)
|
|
const rawError = event.properties.$ai_error
|
|
const message =
|
|
typeof rawError === 'object' &&
|
|
rawError !== null &&
|
|
'message' in rawError &&
|
|
typeof rawError.message === 'string'
|
|
? rawError.message
|
|
: formatAiErrorForDisplay(rawError)
|
|
const key = `${label}::${message}`
|
|
if (!seen.has(key)) {
|
|
seen.add(key)
|
|
ordered.push({ label, message })
|
|
}
|
|
}
|
|
return ordered
|
|
}
|
|
|
|
/**
|
|
* Walk traces in order and hide inputs already shown in an earlier turn.
|
|
* Without this, turn N would re-render turns 1..N-1 because each `$ai_input`
|
|
* carries the full running history.
|
|
*
|
|
* Count-based, not set-based: a message at its Nth occurrence in a turn's
|
|
* input renders iff fewer than N copies have been shown so far. Preserves
|
|
* legitimate repeats within a turn.
|
|
*/
|
|
export function extractSessionTurns(traces: LLMTrace[], fullTraces: Record<string, LLMTrace>): SessionTurn[] {
|
|
const msgCountShown = new Map<string, number>()
|
|
return traces.map((trace) => {
|
|
const fullTrace = fullTraces[trace.id]
|
|
if (!fullTrace) {
|
|
return {
|
|
trace,
|
|
isLoaded: false,
|
|
newInputs: [],
|
|
outputs: [],
|
|
tools: [],
|
|
errors: [],
|
|
}
|
|
}
|
|
const tools = getToolNamesCalledUnique(fullTrace.events ?? [])
|
|
const errors = collectDistinctErrors(fullTrace.events ?? [])
|
|
const userVisibleTurn = pickUserVisibleTurn(fullTrace)
|
|
if (!userVisibleTurn) {
|
|
return {
|
|
trace,
|
|
isLoaded: true,
|
|
newInputs: [],
|
|
outputs: [],
|
|
tools,
|
|
errors,
|
|
}
|
|
}
|
|
const { properties } = userVisibleTurn
|
|
const rawInput = readAiInput(properties)
|
|
const rawOutput = readAiOutput(properties)
|
|
const aiTools = properties.$ai_tools
|
|
|
|
const inputMessages = normalizeMessages(rawInput, 'user', aiTools).messages
|
|
const outputMessages = normalizeMessages(rawOutput, 'assistant').messages
|
|
|
|
const newInputs: CompatMessage[] = []
|
|
const msgCountThisTurn = new Map<string, number>()
|
|
for (const message of inputMessages) {
|
|
const sig = messageSignature(message)
|
|
const turnCount = (msgCountThisTurn.get(sig) ?? 0) + 1
|
|
msgCountThisTurn.set(sig, turnCount)
|
|
const shownCount = msgCountShown.get(sig) ?? 0
|
|
if (turnCount > shownCount) {
|
|
newInputs.push(message)
|
|
msgCountShown.set(sig, turnCount)
|
|
}
|
|
}
|
|
for (const message of outputMessages) {
|
|
const sig = messageSignature(message)
|
|
msgCountShown.set(sig, (msgCountShown.get(sig) ?? 0) + 1)
|
|
}
|
|
|
|
return {
|
|
trace,
|
|
isLoaded: true,
|
|
newInputs,
|
|
outputs: outputMessages,
|
|
userVisibleTurn,
|
|
tools,
|
|
errors,
|
|
}
|
|
})
|
|
}
|