UNPKG

pi-decider

Version:

Decision backends for pi and omp — TypeSafe Jev, OpenRouter's Decisions API, or an OpenAI-compatible chat proxy — exposed as one typed tool (noul / choice / score)

153 lines (141 loc) • 6.2 kB
/** * Text rendering for the `decide` tool result and the `/decide` command. The tool text is what the * model reads back, so it stays compact and machine-parseable; the full structured result travels in * the tool result `details`. */ import { BACKEND_IDS, maskKey, type BackendConfig, type DeciderConfig } from "./config.ts"; import type { JevAnswer, JevQuestion } from "./questions.ts"; import type { CompositeResult, GateRow } from "./shapes.ts"; import type { TokenCounts } from "./usage.ts"; export interface AnswerReport { ids: string[]; questions: Record<string, JevQuestion>; answers: Record<string, JevAnswer>; /** Fatal per-question problems: no usable answer. */ issues: Record<string, string>; /** Non-fatal per-question observations. */ notes: Record<string, string>; } /** One answered batch: every question routed to the same backend + model. */ export interface GroupMeta { backendId: string; label: string; kind: "decisions" | "chat"; requestedModel: string; servedModel?: string; provider?: string; endpoint: string; latencyMs: number; tokens: TokenCounts; costUsd?: number; ids: string[]; notes?: string[]; } export function formatAnswers(report: AnswerReport, metas: GroupMeta[]): string { const lines: string[] = []; for (const meta of metas) { const header: string[] = [ meta.backendId, meta.label, meta.servedModel === undefined || meta.servedModel === meta.requestedModel ? `model ${meta.requestedModel}` : `model ${meta.requestedModel} → ${meta.servedModel}`, ...(meta.provider === undefined ? [] : [`provider ${meta.provider}`]), `${meta.latencyMs} ms`, `tokens ${meta.tokens.input ?? 0} in / ${meta.tokens.output ?? 0} out`, ]; if (meta.costUsd !== undefined) header.push(`cost $${meta.costUsd.toFixed(6)}`); lines.push(header.join(" · ")); if (meta.kind === "chat") { lines.push("note: proxy decision by a general chat model, not a calibrated System One model"); } for (const note of meta.notes ?? []) lines.push(`note: ${note}`); for (const id of meta.ids) lines.push(...renderAnswer(id, report)); } for (const [id, note] of Object.entries(report.notes)) lines.push(`note ${id}: ${note}`); return lines.join("\n"); } export function formatConfigStatus(cfg: DeciderConfig, selected?: BackendConfig): string { const rows: Array<[string, string]> = [ ["config", cfg.configPath], ["backend", `${cfg.backend} (${cfg.backendSource})${selected === undefined ? "" : ` → ${selected.id}`}`], ]; for (const id of BACKEND_IDS) { const backend = cfg.backends[id]; const marker = selected?.id === id ? "*" : " "; rows.push([ `${marker} ${id}`, `${backend.label} · ${backend.kind}`, ]); rows.push([ " key", backend.apiKey === undefined ? `${backend.apiKeySource} (nothing resolved)` : `${backend.apiKeySource} ${maskKey(backend.apiKey)}`, ]); rows.push([" url", `${backend.baseUrl}${backend.path} (${backend.baseUrlSource})`]); rows.push([ " model", backend.model === undefined ? "not set" : `${backend.model} (${backend.modelSource})`, ]); rows.push([ " limits", `timeout ${backend.timeoutMs} ms · retries ${backend.maxRetries} · $${backend.costPerMTokInput}/Mtok in · $${backend.costPerMTokOutput}/Mtok out`, ]); } rows.push(["models path", cfg.modelsPath]); const width = Math.max(...rows.map(([label]) => label.length)); const lines = rows.map(([label, value]) => `${label.padEnd(width)} ${value}`); for (const note of cfg.notes) lines.push(`${"note".padEnd(width)} ${note}`); return lines.join("\n"); } export function formatModelList(title: string, names: string[], limit = 40): string { if (names.length === 0) return `${title}: none reported`; const shown = names.slice(0, limit); const suffix = names.length > shown.length ? `\n… ${names.length - shown.length} more` : ""; return `${title} (${names.length}):\n${shown.join("\n")}${suffix}`; } /** Confidence-gated routing verdicts. */ export function formatGate(rows: GateRow[], threshold: number): string { const lines = [`gate: ${rows.filter((row) => row.pass).length}/${rows.length} pass at ${threshold}`]; for (const row of rows) { const value = row.value === undefined ? "n/a" : String(row.value); lines.push(` ${row.pass ? "PASS" : "HOLD"} ${row.id} ${row.metric}=${value}`); } return lines.join("\n"); } /** Composite scoring: normalized score × weight, then the weighted mean. */ export function formatComposite(result: CompositeResult): string { if (result.rows.length === 0) return "composite: no score answers to combine"; const lines = result.rows.map( (row) => ` ${row.id} normalized ${round(row.normalized, 4)} × weight ${row.weight} = ${round(row.contribution, 4)}`, ); lines.push(`composite score ${round(result.total, 4)} (total weight ${result.totalWeight})`); return lines.join("\n"); } function round(value: number, digits: number): number { const factor = 10 ** digits; return Math.round(value * factor) / factor; } function renderAnswer(id: string, report: AnswerReport): string[] { const answer = report.answers[id]; if (answer === undefined) { return [`${id} [${report.questions[id].type}] ERROR ${report.issues[id] ?? "no answer"}`]; } const confidence = "confidence" in answer && answer.confidence !== undefined ? ` · confidence ${answer.confidence}` : ""; if (answer.type === "noul") return [`${id} [noul] ${answer.noul}${confidence}`]; if (answer.type === "choice") { return [`${id} [choice] ${answer.choice}${confidence}`, ` probabilities: ${renderDistribution(answer.probabilities)}`]; } return [ `${id} [score] ${answer.score}${confidence}`, ` levels: ${Object.entries(answer.legend) .map(([index, level]) => `${index} ${level}`) .join(" · ")}`, ` probabilities: ${renderDistribution(answer.probabilities)}`, ]; } function renderDistribution(probabilities: Record<string, number>): string { return Object.entries(probabilities) .map(([key, value]) => `${key} ${value}`) .join(" · "); }