magnitude-core
Version:
Magnitude e2e testing agent
135 lines (134 loc) • 5.19 kB
JavaScript
/**
* 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;
}