@ddwang/magnitude-core 0.3.1-ddwang.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/desktopActions.d.ts +58 -0
- package/dist/actions/desktopActions.js +154 -0
- package/dist/actions/index.d.ts +26 -0
- package/dist/actions/index.js +11 -0
- package/dist/actions/taskActions.d.ts +8 -0
- package/dist/actions/taskActions.js +27 -0
- package/dist/actions/types.d.ts +31 -0
- package/dist/actions/types.js +5 -0
- package/dist/actions/util.d.ts +20 -0
- package/dist/actions/util.js +297 -0
- package/dist/actions/webActions.d.ts +79 -0
- package/dist/actions/webActions.js +190 -0
- package/dist/agent/browserAgent.d.ts +29 -0
- package/dist/agent/browserAgent.js +122 -0
- package/dist/agent/errors.d.ts +8 -0
- package/dist/agent/errors.js +68 -0
- package/dist/agent/index.d.ts +53 -0
- package/dist/agent/index.js +377 -0
- package/dist/agent/narrator.d.ts +4 -0
- package/dist/agent/narrator.js +63 -0
- package/dist/agent/pause.test.d.ts +1 -0
- package/dist/agent/pause.test.js +194 -0
- package/dist/ai/baml_client/async_client.d.ts +73 -0
- package/dist/ai/baml_client/async_client.js +152 -0
- package/dist/ai/baml_client/async_request.d.ts +35 -0
- package/dist/ai/baml_client/async_request.js +98 -0
- package/dist/ai/baml_client/config.d.ts +11 -0
- package/dist/ai/baml_client/config.js +20 -0
- package/dist/ai/baml_client/globals.d.ts +16 -0
- package/dist/ai/baml_client/globals.js +37 -0
- package/dist/ai/baml_client/index.d.ts +16 -0
- package/dist/ai/baml_client/index.js +41 -0
- package/dist/ai/baml_client/inlinedbaml.d.ts +19 -0
- package/dist/ai/baml_client/inlinedbaml.js +31 -0
- package/dist/ai/baml_client/parser.d.ts +53 -0
- package/dist/ai/baml_client/parser.js +86 -0
- package/dist/ai/baml_client/partial_types.d.ts +61 -0
- package/dist/ai/baml_client/partial_types.js +10 -0
- package/dist/ai/baml_client/sync_client.d.ts +50 -0
- package/dist/ai/baml_client/sync_client.js +100 -0
- package/dist/ai/baml_client/sync_request.d.ts +35 -0
- package/dist/ai/baml_client/sync_request.js +98 -0
- package/dist/ai/baml_client/tracing.d.ts +16 -0
- package/dist/ai/baml_client/tracing.js +18 -0
- package/dist/ai/baml_client/type_builder.d.ts +38 -0
- package/dist/ai/baml_client/type_builder.js +96 -0
- package/dist/ai/baml_client/types.d.ts +69 -0
- package/dist/ai/baml_client/types.js +15 -0
- package/dist/ai/claudeCode.d.ts +1 -0
- package/dist/ai/claudeCode.js +186 -0
- package/dist/ai/modelHarness.d.ts +39 -0
- package/dist/ai/modelHarness.js +189 -0
- package/dist/ai/multiModelHarness.d.ts +27 -0
- package/dist/ai/multiModelHarness.js +52 -0
- package/dist/ai/types.d.ts +90 -0
- package/dist/ai/types.js +5 -0
- package/dist/ai/util.d.ts +13 -0
- package/dist/ai/util.js +165 -0
- package/dist/cli.d.ts +0 -0
- package/dist/cli.js +3 -0
- package/dist/common/actions.d.ts +0 -0
- package/dist/common/actions.js +14 -0
- package/dist/common/events.d.ts +15 -0
- package/dist/common/events.js +3 -0
- package/dist/common/failure.d.ts +68 -0
- package/dist/common/failure.js +1 -0
- package/dist/common/index.d.ts +3 -0
- package/dist/common/index.js +3 -0
- package/dist/common/retry.d.ts +16 -0
- package/dist/common/retry.js +30 -0
- package/dist/common/util.d.ts +23 -0
- package/dist/common/util.js +132 -0
- package/dist/connectors/browserConnector.d.ts +40 -0
- package/dist/connectors/browserConnector.js +127 -0
- package/dist/connectors/desktopConnector.d.ts +58 -0
- package/dist/connectors/desktopConnector.js +68 -0
- package/dist/connectors/index.d.ts +10 -0
- package/dist/connectors/index.js +2 -0
- package/dist/index.cjs +6143 -0
- package/dist/index.d.cts +825 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +26 -0
- package/dist/index.mjs +6097 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +15 -0
- package/dist/memory/agentMemory.d.ts +37 -0
- package/dist/memory/agentMemory.js +121 -0
- package/dist/memory/image.d.ts +26 -0
- package/dist/memory/image.js +106 -0
- package/dist/memory/index.d.ts +3 -0
- package/dist/memory/index.js +3 -0
- package/dist/memory/masking.d.ts +7 -0
- package/dist/memory/masking.js +141 -0
- package/dist/memory/masking.test.d.ts +1 -0
- package/dist/memory/masking.test.js +117 -0
- package/dist/memory/observation.d.ts +38 -0
- package/dist/memory/observation.js +79 -0
- package/dist/memory/rendering/index.d.ts +10 -0
- package/dist/memory/rendering/index.js +13 -0
- package/dist/memory/rendering/renderJsonParts.d.ts +3 -0
- package/dist/memory/rendering/renderJsonParts.js +115 -0
- package/dist/memory/rendering/renderJsonParts.test.d.ts +1 -0
- package/dist/memory/rendering/renderJsonParts.test.js +217 -0
- package/dist/memory/rendering/renderXmlParts.d.ts +7 -0
- package/dist/memory/rendering/renderXmlParts.js +127 -0
- package/dist/memory/serde.d.ts +20 -0
- package/dist/memory/serde.js +81 -0
- package/dist/memory/util.d.ts +2 -0
- package/dist/memory/util.js +17 -0
- package/dist/telemetry/events.d.ts +9 -0
- package/dist/telemetry/events.js +80 -0
- package/dist/telemetry/index.d.ts +6 -0
- package/dist/telemetry/index.js +91 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +1 -0
- package/dist/util.js +12 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/web/browserProvider.d.ts +23 -0
- package/dist/web/browserProvider.js +153 -0
- package/dist/web/harness.d.ts +102 -0
- package/dist/web/harness.js +340 -0
- package/dist/web/stability.d.ts +57 -0
- package/dist/web/stability.js +316 -0
- package/dist/web/tabs.d.ts +58 -0
- package/dist/web/tabs.js +293 -0
- package/dist/web/transformer.d.ts +7 -0
- package/dist/web/transformer.js +41 -0
- package/dist/web/types.d.ts +32 -0
- package/dist/web/types.js +2 -0
- package/dist/web/util.d.ts +3 -0
- package/dist/web/util.js +81 -0
- package/dist/web/visualizer/cursor.d.ts +16 -0
- package/dist/web/visualizer/cursor.js +152 -0
- package/dist/web/visualizer/index.d.ts +22 -0
- package/dist/web/visualizer/index.js +47 -0
- package/dist/web/visualizer/mouseEffects.d.ts +18 -0
- package/dist/web/visualizer/mouseEffects.js +287 -0
- package/dist/web/visualizer/typeEffects.d.ts +10 -0
- package/dist/web/visualizer/typeEffects.js +190 -0
- package/package.json +71 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observations and declarations for what kind of data is "observable" - i.e. can be processed into BAML context or JSON.
|
|
3
|
+
*/
|
|
4
|
+
import { fnv1a32Hex } from '@/util';
|
|
5
|
+
import { observableDataToJson } from './serde';
|
|
6
|
+
import { renderContentParts } from './rendering';
|
|
7
|
+
export class Observation {
|
|
8
|
+
source; // where this observation came from
|
|
9
|
+
role;
|
|
10
|
+
timestamp; // time this observation was made
|
|
11
|
+
content; // the arbitrarily-structured multimedia content of this observation
|
|
12
|
+
retention;
|
|
13
|
+
constructor(source, role, content, retention, timestamp) {
|
|
14
|
+
this.source = source;
|
|
15
|
+
this.role = role;
|
|
16
|
+
this.content = content;
|
|
17
|
+
this.timestamp = timestamp ?? Date.now();
|
|
18
|
+
this.retention = retention;
|
|
19
|
+
}
|
|
20
|
+
static fromConnector(connectorId, content, options) {
|
|
21
|
+
return new Observation(`connector:${connectorId}`, 'user', content, options);
|
|
22
|
+
}
|
|
23
|
+
static fromActionTaken(actionId, content, options) {
|
|
24
|
+
// see notes on fromThought
|
|
25
|
+
return new Observation(`action:taken:${actionId}`, 'user', content, options);
|
|
26
|
+
}
|
|
27
|
+
static fromActionResult(actionId, content, options) {
|
|
28
|
+
return new Observation(`action:result:${actionId}`, 'user', content, options);
|
|
29
|
+
}
|
|
30
|
+
static fromThought(content, options) {
|
|
31
|
+
// may want to reconsider this structure - probably want to show something that actually matches what the assistant sent else it might pattern match.
|
|
32
|
+
// or just do these as user messages.
|
|
33
|
+
// changed to user for now
|
|
34
|
+
return new Observation(`thought`, 'user', content, options);
|
|
35
|
+
}
|
|
36
|
+
toString() {
|
|
37
|
+
// will be pretty ugly usually
|
|
38
|
+
return JSON.stringify(this.content);
|
|
39
|
+
}
|
|
40
|
+
async toJson() {
|
|
41
|
+
return await observableDataToJson(this.content);
|
|
42
|
+
}
|
|
43
|
+
async render(options) {
|
|
44
|
+
return {
|
|
45
|
+
role: this.role,
|
|
46
|
+
cacheControl: options?.cacheControl ?? false,
|
|
47
|
+
content: [...(options?.prefix ?? []), ...(await renderContentParts(this.content, { mode: 'json', indent: 2 })), ...(options?.postfix ?? [])]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// async renderContentParts(): Promise<MultiMediaContentPart[]> {
|
|
51
|
+
// return await renderParts(this.content);
|
|
52
|
+
// }
|
|
53
|
+
// async toContext(): Promise<BamlRenderable[]> {
|
|
54
|
+
// return await observableDataToContext(this.data);
|
|
55
|
+
// }
|
|
56
|
+
async hash() {
|
|
57
|
+
// build str of all text+img content
|
|
58
|
+
const stringifiedContent = JSON.stringify(await this.toJson());
|
|
59
|
+
return fnv1a32Hex(stringifiedContent);
|
|
60
|
+
}
|
|
61
|
+
async equals(obs) {
|
|
62
|
+
// TODO: deep eq for both text and img content
|
|
63
|
+
return (await this.hash()) == (await obs.hash());
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// export async function renderObservations(observations: Observation[]): Promise<BamlRenderable[]> {
|
|
67
|
+
// /**
|
|
68
|
+
// * Refines observations (according to observation culling options) and converts to BAML-renderable content
|
|
69
|
+
// */
|
|
70
|
+
// const filteredObservations = await filterObservations(observations);
|
|
71
|
+
// let content: BamlRenderable[] = [];
|
|
72
|
+
// for (const obs of filteredObservations) {
|
|
73
|
+
// if (obs.source.startsWith('action:taken') || obs.source.startsWith('thought')) {
|
|
74
|
+
// content.push(`[${new Date(obs.timestamp).toTimeString().split(' ')[0]}]: `);
|
|
75
|
+
// }
|
|
76
|
+
// content = [...content, ...await obs.toContext(), "\n"];
|
|
77
|
+
// }
|
|
78
|
+
// return content;
|
|
79
|
+
// }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { RenderableContent } from "@/memory/observation";
|
|
2
|
+
import { Image as BamlImage } from '@boundaryml/baml';
|
|
3
|
+
export type MultiMediaContentPart = BamlImage | string;
|
|
4
|
+
export type RenderOptions = {
|
|
5
|
+
mode: 'json';
|
|
6
|
+
indent: 0 | 2 | 4;
|
|
7
|
+
} | {
|
|
8
|
+
mode: 'xml';
|
|
9
|
+
};
|
|
10
|
+
export declare function renderContentParts(data: RenderableContent, options: RenderOptions): Promise<import("./renderXmlParts").MultiMediaContentPart[]>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { renderXmlParts } from "./renderXmlParts";
|
|
2
|
+
import { renderJsonParts } from "./renderJsonParts";
|
|
3
|
+
export async function renderContentParts(data, options) {
|
|
4
|
+
if (options.mode === 'json') {
|
|
5
|
+
return await renderJsonParts(data, options.indent);
|
|
6
|
+
}
|
|
7
|
+
else if (options.mode === 'xml') {
|
|
8
|
+
return await renderXmlParts(data);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
throw new Error(`Invalid render mode ${options.mode}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Image } from "@/memory/image";
|
|
2
|
+
import { Image as BamlImage } from '@boundaryml/baml';
|
|
3
|
+
async function buildJsonPartsRecursive(data, partsList, indent, currentLevel = 0, isRoot = false) {
|
|
4
|
+
if (data instanceof Image) {
|
|
5
|
+
const bamlImg = await data.toBaml();
|
|
6
|
+
if (bamlImg) {
|
|
7
|
+
partsList.push(bamlImg);
|
|
8
|
+
}
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (data instanceof BamlImage) {
|
|
12
|
+
partsList.push(data);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (data === null) {
|
|
16
|
+
partsList.push('null');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (data === undefined) {
|
|
20
|
+
return; // Undefined values are omitted in JSON
|
|
21
|
+
}
|
|
22
|
+
if (typeof data === 'string') {
|
|
23
|
+
if (isRoot) {
|
|
24
|
+
partsList.push(data);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
partsList.push(JSON.stringify(data));
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (typeof data === 'number' || typeof data === 'boolean') {
|
|
32
|
+
partsList.push(String(data));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const newline = indent > 0 ? '\n' : '';
|
|
36
|
+
const spacing = indent > 0 ? ' '.repeat(currentLevel * indent) : '';
|
|
37
|
+
const nextSpacing = indent > 0 ? ' '.repeat((currentLevel + 1) * indent) : '';
|
|
38
|
+
if (Array.isArray(data)) {
|
|
39
|
+
partsList.push('[');
|
|
40
|
+
for (let index = 0; index < data.length; index++) {
|
|
41
|
+
const item = data[index];
|
|
42
|
+
if (item !== undefined) { // Skip undefined values in arrays
|
|
43
|
+
if (index > 0) {
|
|
44
|
+
partsList.push(',');
|
|
45
|
+
}
|
|
46
|
+
if (indent > 0) {
|
|
47
|
+
partsList.push(newline + nextSpacing);
|
|
48
|
+
}
|
|
49
|
+
else if (index > 0) {
|
|
50
|
+
partsList.push(' ');
|
|
51
|
+
}
|
|
52
|
+
await buildJsonPartsRecursive(item, partsList, indent, currentLevel + 1, false);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (data.length > 0 && indent > 0) {
|
|
56
|
+
partsList.push(newline + spacing);
|
|
57
|
+
}
|
|
58
|
+
partsList.push(']');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (typeof data === 'object' && data !== null) {
|
|
62
|
+
partsList.push('{');
|
|
63
|
+
const entries = Object.entries(data).filter(([, val]) => val !== undefined);
|
|
64
|
+
for (let index = 0; index < entries.length; index++) {
|
|
65
|
+
const [key, value] = entries[index];
|
|
66
|
+
if (index > 0) {
|
|
67
|
+
partsList.push(',');
|
|
68
|
+
}
|
|
69
|
+
if (indent > 0) {
|
|
70
|
+
partsList.push(newline + nextSpacing);
|
|
71
|
+
}
|
|
72
|
+
else if (index > 0) {
|
|
73
|
+
partsList.push(' ');
|
|
74
|
+
}
|
|
75
|
+
// Add the key
|
|
76
|
+
partsList.push(JSON.stringify(key));
|
|
77
|
+
partsList.push(': ');
|
|
78
|
+
// Add the value
|
|
79
|
+
await buildJsonPartsRecursive(value, partsList, indent, currentLevel + 1, false);
|
|
80
|
+
}
|
|
81
|
+
if (entries.length > 0 && indent > 0) {
|
|
82
|
+
partsList.push(newline + spacing);
|
|
83
|
+
}
|
|
84
|
+
partsList.push('}');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`Object type not supported for JSON rendering: '${typeof data}'. Data: ${data}`);
|
|
88
|
+
}
|
|
89
|
+
export async function renderJsonParts(data, indent) {
|
|
90
|
+
const rawList = [];
|
|
91
|
+
await buildJsonPartsRecursive(data, rawList, indent, 0, true);
|
|
92
|
+
// Merge adjacent strings
|
|
93
|
+
if (rawList.length === 0) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const mergedList = [];
|
|
97
|
+
let currentString = "";
|
|
98
|
+
for (const item of rawList) {
|
|
99
|
+
if (typeof item === 'string') {
|
|
100
|
+
currentString += item;
|
|
101
|
+
}
|
|
102
|
+
else { // It's a BamlImage
|
|
103
|
+
if (currentString.length > 0) {
|
|
104
|
+
mergedList.push(currentString);
|
|
105
|
+
}
|
|
106
|
+
currentString = ""; // Reset accumulator
|
|
107
|
+
mergedList.push(item); // Push the BamlImage
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Add any trailing string
|
|
111
|
+
if (currentString.length > 0) {
|
|
112
|
+
mergedList.push(currentString);
|
|
113
|
+
}
|
|
114
|
+
return mergedList;
|
|
115
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { renderJsonParts } from './renderJsonParts';
|
|
3
|
+
import { Image } from '@/memory/image';
|
|
4
|
+
// Create a mock Image that will convert to BamlImage
|
|
5
|
+
class MockImage extends Image {
|
|
6
|
+
id;
|
|
7
|
+
constructor(id) {
|
|
8
|
+
// Pass a dummy Sharp instance
|
|
9
|
+
super(null);
|
|
10
|
+
this.id = id;
|
|
11
|
+
}
|
|
12
|
+
async toBaml() {
|
|
13
|
+
// Return a mock BamlImage
|
|
14
|
+
return {
|
|
15
|
+
__mockBamlImage: true,
|
|
16
|
+
id: this.id
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
describe('renderJsonParts', () => {
|
|
21
|
+
test('renders primitive types correctly', async () => {
|
|
22
|
+
expect(await renderJsonParts('hello', 0)).toEqual(['hello']);
|
|
23
|
+
expect(await renderJsonParts(42, 0)).toEqual(['42']);
|
|
24
|
+
expect(await renderJsonParts(true, 0)).toEqual(['true']);
|
|
25
|
+
expect(await renderJsonParts(false, 0)).toEqual(['false']);
|
|
26
|
+
expect(await renderJsonParts(null, 0)).toEqual(['null']);
|
|
27
|
+
});
|
|
28
|
+
test('renders root-level strings without quotes', async () => {
|
|
29
|
+
expect(await renderJsonParts('CLICK THE UNICORN', 0)).toEqual(['CLICK THE UNICORN']);
|
|
30
|
+
expect(await renderJsonParts('Hello World', 0)).toEqual(['Hello World']);
|
|
31
|
+
expect(await renderJsonParts('{"already":"json"}', 0)).toEqual(['{"already":"json"}']);
|
|
32
|
+
});
|
|
33
|
+
test('properly quotes strings inside objects and arrays', async () => {
|
|
34
|
+
// Strings in objects should be quoted
|
|
35
|
+
expect(await renderJsonParts({ message: 'hello' }, 0)).toEqual(['{"message": "hello"}']);
|
|
36
|
+
expect(await renderJsonParts({ name: 'John', city: 'NYC' }, 0)).toEqual(['{"name": "John", "city": "NYC"}']);
|
|
37
|
+
// Strings in arrays should be quoted
|
|
38
|
+
expect(await renderJsonParts(['apple', 'banana'], 0)).toEqual(['["apple", "banana"]']);
|
|
39
|
+
expect(await renderJsonParts(['hello', 123, 'world'], 0)).toEqual(['["hello", 123, "world"]']);
|
|
40
|
+
// Nested structures
|
|
41
|
+
expect(await renderJsonParts({ items: ['a', 'b'], text: 'test' }, 0)).toEqual(['{"items": ["a", "b"], "text": "test"}']);
|
|
42
|
+
});
|
|
43
|
+
test('renders empty structures', async () => {
|
|
44
|
+
expect(await renderJsonParts([], 0)).toEqual(['[]']);
|
|
45
|
+
expect(await renderJsonParts({}, 0)).toEqual(['{}']);
|
|
46
|
+
});
|
|
47
|
+
test('renders simple arrays', async () => {
|
|
48
|
+
expect(await renderJsonParts([1, 2, 3], 0)).toEqual(['[1, 2, 3]']);
|
|
49
|
+
expect(await renderJsonParts(['a', 'b', 'c'], 0)).toEqual(['["a", "b", "c"]']);
|
|
50
|
+
expect(await renderJsonParts([true, false, null], 0)).toEqual(['[true, false, null]']);
|
|
51
|
+
});
|
|
52
|
+
test('renders simple objects', async () => {
|
|
53
|
+
expect(await renderJsonParts({ name: 'John', age: 30 }, 0)).toEqual(['{"name": "John", "age": 30}']);
|
|
54
|
+
expect(await renderJsonParts({ a: 1, b: true, c: null }, 0)).toEqual(['{"a": 1, "b": true, "c": null}']);
|
|
55
|
+
});
|
|
56
|
+
test('handles undefined values correctly', async () => {
|
|
57
|
+
// Undefined at root level returns empty array
|
|
58
|
+
expect(await renderJsonParts(undefined, 0)).toEqual([]);
|
|
59
|
+
// Undefined in arrays is skipped
|
|
60
|
+
expect(await renderJsonParts([1, undefined, 3], 0)).toEqual(['[1, 3]']);
|
|
61
|
+
// Undefined in objects is omitted
|
|
62
|
+
expect(await renderJsonParts({ a: 1, b: undefined, c: 3 }, 0)).toEqual(['{"a": 1, "c": 3}']);
|
|
63
|
+
});
|
|
64
|
+
test('renders nested structures', async () => {
|
|
65
|
+
const nested = {
|
|
66
|
+
user: {
|
|
67
|
+
name: 'Alice',
|
|
68
|
+
scores: [95, 87, 92],
|
|
69
|
+
active: true
|
|
70
|
+
},
|
|
71
|
+
metadata: {
|
|
72
|
+
created: '2024-01-01',
|
|
73
|
+
tags: ['important', 'verified']
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const result = await renderJsonParts(nested, 0);
|
|
77
|
+
expect(result).toEqual(['{"user": {"name": "Alice", "scores": [95, 87, 92], "active": true}, "metadata": {"created": "2024-01-01", "tags": ["important", "verified"]}}']);
|
|
78
|
+
});
|
|
79
|
+
test('splits JSON around BamlImage in object', async () => {
|
|
80
|
+
const mockImage = new MockImage('img1');
|
|
81
|
+
const data = {
|
|
82
|
+
title: 'Product',
|
|
83
|
+
image: mockImage,
|
|
84
|
+
price: 29.99
|
|
85
|
+
};
|
|
86
|
+
const result = await renderJsonParts(data, 0);
|
|
87
|
+
expect(result.length).toBe(3);
|
|
88
|
+
expect(result[0]).toBe('{"title": "Product", "image": ');
|
|
89
|
+
expect(result[1]).toHaveProperty('__mockBamlImage', true);
|
|
90
|
+
expect(result[2]).toBe(', "price": 29.99}');
|
|
91
|
+
});
|
|
92
|
+
test('splits JSON around multiple BamlImages in object', async () => {
|
|
93
|
+
const mockImage1 = new MockImage('img1');
|
|
94
|
+
const mockImage2 = new MockImage('img2');
|
|
95
|
+
const data = {
|
|
96
|
+
before: 'text',
|
|
97
|
+
image1: mockImage1,
|
|
98
|
+
middle: 'more text',
|
|
99
|
+
image2: mockImage2,
|
|
100
|
+
after: 'final'
|
|
101
|
+
};
|
|
102
|
+
const result = await renderJsonParts(data, 0);
|
|
103
|
+
expect(result.length).toBe(5);
|
|
104
|
+
expect(result[0]).toBe('{"before": "text", "image1": ');
|
|
105
|
+
expect(result[1]).toHaveProperty('__mockBamlImage', true);
|
|
106
|
+
expect(result[2]).toBe(', "middle": "more text", "image2": ');
|
|
107
|
+
expect(result[3]).toHaveProperty('__mockBamlImage', true);
|
|
108
|
+
expect(result[4]).toBe(', "after": "final"}');
|
|
109
|
+
});
|
|
110
|
+
test('splits JSON around BamlImage in array', async () => {
|
|
111
|
+
const mockImage = new MockImage('img1');
|
|
112
|
+
const data = ['start', mockImage, 'end'];
|
|
113
|
+
const result = await renderJsonParts(data, 0);
|
|
114
|
+
expect(result.length).toBe(3);
|
|
115
|
+
expect(result[0]).toBe('["start", ');
|
|
116
|
+
expect(result[1]).toHaveProperty('__mockBamlImage', true);
|
|
117
|
+
expect(result[2]).toBe(', "end"]');
|
|
118
|
+
});
|
|
119
|
+
test('handles array with only BamlImages', async () => {
|
|
120
|
+
const mockImage1 = new MockImage('img1');
|
|
121
|
+
const mockImage2 = new MockImage('img2');
|
|
122
|
+
const data = [mockImage1, mockImage2];
|
|
123
|
+
const result = await renderJsonParts(data, 0);
|
|
124
|
+
expect(result.length).toBe(5);
|
|
125
|
+
expect(result[0]).toBe('[');
|
|
126
|
+
expect(result[1]).toHaveProperty('__mockBamlImage', true);
|
|
127
|
+
expect(result[2]).toBe(', ');
|
|
128
|
+
expect(result[3]).toHaveProperty('__mockBamlImage', true);
|
|
129
|
+
expect(result[4]).toBe(']');
|
|
130
|
+
});
|
|
131
|
+
test('handles nested structures with images', async () => {
|
|
132
|
+
const mockImage = new MockImage('img1');
|
|
133
|
+
const data = {
|
|
134
|
+
user: {
|
|
135
|
+
name: 'Bob',
|
|
136
|
+
avatar: mockImage,
|
|
137
|
+
settings: {
|
|
138
|
+
theme: 'dark'
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
posts: [
|
|
142
|
+
{ id: 1, image: mockImage, text: 'Hello' }
|
|
143
|
+
]
|
|
144
|
+
};
|
|
145
|
+
const result = await renderJsonParts(data, 0);
|
|
146
|
+
// Should have multiple parts due to images
|
|
147
|
+
expect(result.length).toBeGreaterThan(1);
|
|
148
|
+
// First part should start with JSON
|
|
149
|
+
expect(result[0]).toMatch(/^{"user": /);
|
|
150
|
+
// Should contain the mock images
|
|
151
|
+
const imageParts = result.filter(part => part && typeof part === 'object' && '__mockBamlImage' in part);
|
|
152
|
+
expect(imageParts.length).toBe(2);
|
|
153
|
+
});
|
|
154
|
+
test('handles edge case with image as first array element', async () => {
|
|
155
|
+
const mockImage = new MockImage('img1');
|
|
156
|
+
const data = [mockImage, 'text'];
|
|
157
|
+
const result = await renderJsonParts(data, 0);
|
|
158
|
+
expect(result.length).toBe(3);
|
|
159
|
+
expect(result[0]).toBe('[');
|
|
160
|
+
expect(result[1]).toHaveProperty('__mockBamlImage', true);
|
|
161
|
+
expect(result[2]).toBe(', "text"]');
|
|
162
|
+
});
|
|
163
|
+
test('handles edge case with image as last array element', async () => {
|
|
164
|
+
const mockImage = new MockImage('img1');
|
|
165
|
+
const data = ['text', mockImage];
|
|
166
|
+
const result = await renderJsonParts(data, 0);
|
|
167
|
+
expect(result.length).toBe(3);
|
|
168
|
+
expect(result[0]).toBe('["text", ');
|
|
169
|
+
expect(result[1]).toHaveProperty('__mockBamlImage', true);
|
|
170
|
+
expect(result[2]).toBe(']');
|
|
171
|
+
});
|
|
172
|
+
test('properly escapes special characters in strings', async () => {
|
|
173
|
+
const data = {
|
|
174
|
+
quote: 'He said "Hello"',
|
|
175
|
+
newline: 'Line 1\nLine 2',
|
|
176
|
+
tab: 'Col1\tCol2',
|
|
177
|
+
backslash: 'C:\\Users\\Path'
|
|
178
|
+
};
|
|
179
|
+
const result = await renderJsonParts(data, 0);
|
|
180
|
+
expect(result[0]).toContain('\\"Hello\\"');
|
|
181
|
+
expect(result[0]).toContain('\\n');
|
|
182
|
+
expect(result[0]).toContain('\\t');
|
|
183
|
+
expect(result[0]).toContain('\\\\');
|
|
184
|
+
});
|
|
185
|
+
test('renders with proper indentation when indent is 2', async () => {
|
|
186
|
+
const data = {
|
|
187
|
+
name: 'John',
|
|
188
|
+
age: 30,
|
|
189
|
+
hobbies: ['reading', 'coding']
|
|
190
|
+
};
|
|
191
|
+
const result = await renderJsonParts(data, 2);
|
|
192
|
+
expect(result).toEqual(['{\n "name": "John",\n "age": 30,\n "hobbies": [\n "reading",\n "coding"\n ]\n}']);
|
|
193
|
+
});
|
|
194
|
+
test('renders with proper indentation when indent is 4', async () => {
|
|
195
|
+
const data = {
|
|
196
|
+
user: {
|
|
197
|
+
name: 'Alice',
|
|
198
|
+
active: true
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
const result = await renderJsonParts(data, 4);
|
|
202
|
+
expect(result).toEqual(['{\n "user": {\n "name": "Alice",\n "active": true\n }\n}']);
|
|
203
|
+
});
|
|
204
|
+
test('handles indentation with images', async () => {
|
|
205
|
+
const mockImage = new MockImage('img1');
|
|
206
|
+
const data = {
|
|
207
|
+
title: 'Product',
|
|
208
|
+
image: mockImage,
|
|
209
|
+
price: 29.99
|
|
210
|
+
};
|
|
211
|
+
const result = await renderJsonParts(data, 2);
|
|
212
|
+
expect(result.length).toBe(3);
|
|
213
|
+
expect(result[0]).toBe('{\n "title": "Product",\n "image": ');
|
|
214
|
+
expect(result[1]).toHaveProperty('__mockBamlImage', true);
|
|
215
|
+
expect(result[2]).toBe(',\n "price": 29.99\n}');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert observations to form that can be passed as BAML context, rendered as a custom XML-like structure.
|
|
3
|
+
*/
|
|
4
|
+
import { Image as BamlImage } from '@boundaryml/baml';
|
|
5
|
+
import { RenderableContent } from '@/memory/observation';
|
|
6
|
+
export type MultiMediaContentPart = BamlImage | string;
|
|
7
|
+
export declare function renderXmlParts(data: RenderableContent): Promise<MultiMediaContentPart[]>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert observations to form that can be passed as BAML context, rendered as a custom XML-like structure.
|
|
3
|
+
*/
|
|
4
|
+
import { Image as BamlImage } from '@boundaryml/baml';
|
|
5
|
+
import { Image } from '@/memory/image';
|
|
6
|
+
//export type BamlRenderable = BamlImage | string;
|
|
7
|
+
// export type BamlRenderable = {
|
|
8
|
+
// role: ''
|
|
9
|
+
// content: (BamlImage | string)[]
|
|
10
|
+
// }
|
|
11
|
+
async function buildXmlPartsRecursive(data, indentLevel, partsList, isInsideList = false) {
|
|
12
|
+
const indent = ' '.repeat(indentLevel);
|
|
13
|
+
if (data instanceof Image) {
|
|
14
|
+
const bamlImg = await data.toBaml();
|
|
15
|
+
if (bamlImg) {
|
|
16
|
+
partsList.push(bamlImg);
|
|
17
|
+
}
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (data instanceof BamlImage) {
|
|
21
|
+
partsList.push(data);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean' || data === null) {
|
|
25
|
+
partsList.push(String(data)); // No XML escaping, direct string conversion
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (data === undefined) {
|
|
29
|
+
return; // Undefined values are omitted
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(data)) {
|
|
32
|
+
// data.forEach(async (item, index) => {
|
|
33
|
+
// await buildXmlPartsRecursive(item, indentLevel, partsList, true);
|
|
34
|
+
// if (index < data.length - 1) {
|
|
35
|
+
// partsList.push("\n");
|
|
36
|
+
// }
|
|
37
|
+
// });
|
|
38
|
+
for (let index = 0; index < data.length; index++) {
|
|
39
|
+
await buildXmlPartsRecursive(data[index], indentLevel, partsList, true);
|
|
40
|
+
if (index < data.length - 1) {
|
|
41
|
+
partsList.push("\n");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (typeof data === 'object' && data !== null) {
|
|
47
|
+
const objectEntries = Object.entries(data).filter(([, val]) => val !== undefined);
|
|
48
|
+
objectEntries.forEach(async ([key, value], entryIndex) => {
|
|
49
|
+
const tagName = key; // Use key directly as tag name
|
|
50
|
+
const currentValueParts = [];
|
|
51
|
+
await buildXmlPartsRecursive(value, indentLevel + 1, currentValueParts, false);
|
|
52
|
+
// Merge adjacent strings in currentValueParts for easier analysis
|
|
53
|
+
const mergedValueParts = [];
|
|
54
|
+
let currentStr = "";
|
|
55
|
+
for (const part of currentValueParts) {
|
|
56
|
+
if (typeof part === 'string') {
|
|
57
|
+
currentStr += part;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
if (currentStr)
|
|
61
|
+
mergedValueParts.push(currentStr);
|
|
62
|
+
currentStr = "";
|
|
63
|
+
mergedValueParts.push(part);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (currentStr)
|
|
67
|
+
mergedValueParts.push(currentStr);
|
|
68
|
+
// Apply styling rules
|
|
69
|
+
if (mergedValueParts.length === 1 && mergedValueParts[0] instanceof BamlImage) {
|
|
70
|
+
partsList.push(`${indent}<${tagName}>`);
|
|
71
|
+
partsList.push(mergedValueParts[0]); // The BamlImage
|
|
72
|
+
partsList.push(`</${tagName}>`);
|
|
73
|
+
}
|
|
74
|
+
else if (mergedValueParts.length === 1 && typeof mergedValueParts[0] === 'string') {
|
|
75
|
+
const contentStr = mergedValueParts[0];
|
|
76
|
+
if (contentStr.includes('\n')) {
|
|
77
|
+
partsList.push(`${indent}<${tagName}>\n${contentStr}\n${indent}</${tagName}>`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
partsList.push(`${indent}<${tagName}>${contentStr}</${tagName}>`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else { // Empty content or multiple parts (complex/nested)
|
|
84
|
+
partsList.push(`${indent}<${tagName}>\n`);
|
|
85
|
+
mergedValueParts.forEach((part, partIdx) => {
|
|
86
|
+
partsList.push(part);
|
|
87
|
+
if (partIdx < mergedValueParts.length - 1) {
|
|
88
|
+
partsList.push("\n");
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
partsList.push(`\n${indent}</${tagName}>`);
|
|
92
|
+
}
|
|
93
|
+
if (entryIndex < objectEntries.length - 1) {
|
|
94
|
+
partsList.push("\n"); // Newline between sibling XML elements
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`Object type not supported for LLM context: ${typeof data}`);
|
|
100
|
+
}
|
|
101
|
+
export async function renderXmlParts(data) {
|
|
102
|
+
const rawList = [];
|
|
103
|
+
await buildXmlPartsRecursive(data, 0, rawList);
|
|
104
|
+
// Merge adjacent strings
|
|
105
|
+
if (rawList.length === 0) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
const mergedList = [];
|
|
109
|
+
let currentString = "";
|
|
110
|
+
for (const item of rawList) {
|
|
111
|
+
if (typeof item === 'string') {
|
|
112
|
+
currentString += item;
|
|
113
|
+
}
|
|
114
|
+
else { // It's a BamlImage
|
|
115
|
+
if (currentString.length > 0) {
|
|
116
|
+
mergedList.push(currentString);
|
|
117
|
+
}
|
|
118
|
+
currentString = ""; // Reset accumulator
|
|
119
|
+
mergedList.push(item); // Push the BamlImage
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Add any trailing string
|
|
123
|
+
if (currentString.length > 0) {
|
|
124
|
+
mergedList.push(currentString);
|
|
125
|
+
}
|
|
126
|
+
return mergedList;
|
|
127
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { RenderableContent } from "./observation";
|
|
2
|
+
export type StoredMedia = {
|
|
3
|
+
type: 'media';
|
|
4
|
+
format: string;
|
|
5
|
+
storage: 'base64';
|
|
6
|
+
base64: string;
|
|
7
|
+
};
|
|
8
|
+
export interface StoredPrimitive {
|
|
9
|
+
type: 'primitive';
|
|
10
|
+
content: string | boolean | number;
|
|
11
|
+
}
|
|
12
|
+
type MultiMediaPrimitive = StoredMedia | StoredPrimitive | undefined | null;
|
|
13
|
+
type MultiMediaArray = Array<MultiMediaJson>;
|
|
14
|
+
export type MultiMediaObject = {
|
|
15
|
+
[key: string]: MultiMediaJson;
|
|
16
|
+
};
|
|
17
|
+
export type MultiMediaJson = MultiMediaPrimitive | MultiMediaArray | MultiMediaObject;
|
|
18
|
+
export declare function observableDataToJson(data: RenderableContent): Promise<MultiMediaJson>;
|
|
19
|
+
export declare function jsonToObservableData(data: MultiMediaJson): Promise<RenderableContent>;
|
|
20
|
+
export {};
|