UNPKG

magnitude-core

Version:
135 lines (134 loc) 5.19 kB
/** * Observations and declarations for what kind of data is "observable" - i.e. can be processed into BAML context or JSON. */ import { fnv1a32Hex } from '@/util'; import { observableDataToContext } from './context'; import { observableDataToJson } from './serde'; export class Observation { source; timestamp; data; options; constructor(source, data, options) { this.source = source; this.data = data; this.timestamp = Date.now(); this.options = options; } static fromConnector(connectorId, data, options) { return new Observation(`connector:${connectorId}`, data, options); } static fromActionTaken(actionId, data, options) { return new Observation(`action:taken:${actionId}`, data, options); } static fromActionResult(actionId, data, options) { return new Observation(`action:result:${actionId}`, data, options); } static fromThought(data, options) { return new Observation(`thought`, data, options); } toString() { // will be pretty ugly usually return JSON.stringify(this.data); } async toJson() { return await observableDataToJson(this.data); } async toContext() { return await observableDataToContext(this.data); } async hash() { // build str of all text+img content const stringifiedContent = JSON.stringify(await this.toJson()); return fnv1a32Hex(stringifiedContent); } async equals(obs) { // TODO: deep eq for both text and img content return (await this.hash()) == (await obs.hash()); } } async function filterObservations(observations) { /** * (LLM) * Filters an array of Observations based on type-specific `dedupe` and `limit` options * specified in `obs.options`. For each type: * 1. If `dedupe` is true, removes older adjacent identical observations (checked via `obs.equals()`), * keeping the last in such a sequence. * 2. If `limit` is set, keeps only the last N observations of that type. * Deduplication is applied before limiting for each type. * Untyped observations (no `obs.options.type`) are passed through unfiltered. * The function preserves the original relative order of the surviving observations. */ const observationsByType = new Map(); const untypedObservations = []; for (const obs of observations) { if (obs.options && obs.options.type) { const type = obs.options.type; if (!observationsByType.has(type)) { observationsByType.set(type, { obsList: [], limit: obs.options.limit, dedupe: obs.options.dedupe, }); } observationsByType.get(type).obsList.push(obs); } else { untypedObservations.push(obs); } } for (const data of observationsByType.values()) { let currentTypedObservations = data.obsList; if (data.dedupe && currentTypedObservations.length > 1) { const dedupedList = []; dedupedList.unshift(currentTypedObservations[currentTypedObservations.length - 1]); for (let i = currentTypedObservations.length - 2; i >= 0; i--) { const currentObs = currentTypedObservations[i]; const lastKeptObs = dedupedList[0]; if (!(await currentObs.equals(lastKeptObs))) { dedupedList.unshift(currentObs); } } currentTypedObservations = dedupedList; } if (data.limit !== undefined && data.limit >= 0) { if (data.limit === 0) { currentTypedObservations = []; } else { currentTypedObservations = currentTypedObservations.slice(-data.limit); } } data.obsList = currentTypedObservations; } const finalObservationsToRender = []; const survivorSets = new Map(); for (const [type, data] of observationsByType.entries()) { survivorSets.set(type, new Set(data.obsList)); } for (const originalObs of observations) { if (originalObs.options && originalObs.options.type) { if (survivorSets.get(originalObs.options.type)?.has(originalObs)) { finalObservationsToRender.push(originalObs); } } else { finalObservationsToRender.push(originalObs); } } return finalObservationsToRender; } export async function renderObservations(observations) { /** * Refines observations (according to observation culling options) and converts to BAML-renderable content */ const filteredObservations = await filterObservations(observations); let content = []; for (const obs of filteredObservations) { if (obs.source.startsWith('action:taken') || obs.source.startsWith('thought')) { content.push(`[${new Date(obs.timestamp).toTimeString().split(' ')[0]}]: `); } content = [...content, ...await obs.toContext(), "\n"]; } return content; }