Skip to content

Use Proxy to allow for arbitrary property/array access #96

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: liam/future-types
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions examples/arbitrary-future-properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env -S npx ts-node --transpileOnly

import { Substrate, Box, ComputeText, ComputeJSON, sb } from "substrate";

async function main() {
const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];

const substrate = new Substrate({
apiKey: SUBSTRATE_API_KEY,
});

const numbers = new Box({
value: [0, 1],
});

const latin = new Box({
value: ["a", "b"],
});

const greek = new Box({
value: {
a: "α",
b: "β",
},
});

const proxyAccessor = new Box({ value: { property: "text" } });

const d = new ComputeText({
prompt: "What is the character for the Latin 'D' in the Cyrillic alphabet? (just tell me the character only)",
max_tokens: 1,
});

const three = new ComputeText({
prompt: "What number comes after '2'? (just tell me the character only)",
max_tokens: 1,
});
const number3 = sb.jq<number>(three.future, ".text | tonumber");

const hebrew = new ComputeJSON({
prompt: "what are the characters of the Hebrew alphabet (in unicode, eg. א )?",
json_schema: {
type: "object",
properties: {
characters: {
type: "array",
items: {
type: "string",
description: "single character",
}
}
}
}
});

const result = new Box({
value: {
a: latin.future.value[numbers.future.value[0]],
b: greek.future.value[latin.future.value[1]],
c: hebrew.future.json_object.characters[number3 as any],
ab: sb.concat(greek.future.value.a, greek.future.value.b),
d: sb.get<string>(d.future, proxyAccessor.future.value.property),
},
});

const res = await substrate.run(result);
console.log(res.get(result));
}
main();
7 changes: 6 additions & 1 deletion examples/box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ async function main() {
}, {}),
});

const res = await substrate.run(box);
const box2 = new Box({
value: box.future.value.swedish,
});

const res = await substrate.run(box, box2);

console.log({ box: res.get(box) });
console.log({ box2: res.get(box2) });
}
main();
30 changes: 30 additions & 0 deletions examples/bug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env -S npx ts-node --transpileOnly

import { Substrate, Box, sb } from "substrate";

async function main() {
const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];

const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });

const data = new Box({
value: {
letters: ["a", "b"],
index: 0,
},
});

const selected = new Box({
value: {
example1: sb.concat(
data.future.value.letters[0],
data.future.value.letters[1],
),
// example2: data.future.value.letters[data.future.value.index],
},
});

const res = await substrate.run(selected);
console.log(JSON.stringify(res.json, null, 2));
}
main();
15 changes: 6 additions & 9 deletions examples/if.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ async function main() {
});

const [jupiter, mars] = sizes as [ComputeJSON, ComputeJSON];
const radius = (p: ComputeJSON) =>
p.future.json_object.get("radius") as unknown as number;
const radius = (p: ComputeJSON) => p.future.json_object.radius;

const comparison = new ComputeJSON({
prompt: sb.interpolate`Is ${radius(jupiter)} > ${radius(mars)}?`,
Expand All @@ -45,16 +44,14 @@ async function main() {
},
});

const result = new If({
condition: comparison.future.json_object.get("isGreaterThan") as any,
value_if_true: jupiter.future.json_object,
value_if_false: mars.future.json_object,
const planetName = new If({
condition: comparison.future.json_object.isGreaterThan,
value_if_true: jupiter.future.json_object.planetName,
value_if_false: mars.future.json_object.planetName,
});

const output = new Box({
value: sb.interpolate`The bigger planet is ${result.future.result.get(
"planetName",
)}!`,
value: sb.interpolate`The bigger planet is ${planetName.future.result}!`,
});

const res = await substrate.run(output);
Expand Down
4 changes: 2 additions & 2 deletions examples/image-generation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env -S npx ts-node --transpileOnly

import { Substrate, ComputeText, GenerateImage } from "substrate";
import { Substrate, ComputeText, GenerateImage, sb } from "substrate";

async function main() {
const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
Expand All @@ -22,7 +22,7 @@ async function main() {

const images = styles.map((style) => {
return new GenerateImage({
prompt: scene.future.text.concat(` render in a ((${style})) style`),
prompt: sb.concat(scene.future.text, ` render in a "${style}" style`),
store: "hosted",
});
});
Expand Down
4 changes: 2 additions & 2 deletions examples/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ async function main() {
max_tokens: 800,
});

const name = author.future.json_object.get("name");
const bio = author.future.json_object.get("bio");
const name = author.future.json_object.name;
const bio = author.future.json_object.bio;

const report = new ComputeText({
prompt: sb.interpolate`Write a short summary about ${name} and make sure to use the following bio: ${bio}`,
Expand Down
6 changes: 3 additions & 3 deletions examples/large-run.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env -S npx ts-node --transpileOnly

import { Substrate, ComputeText } from "substrate";
import { Substrate, ComputeText, sb } from "substrate";

async function main() {
const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
Expand All @@ -9,10 +9,10 @@ async function main() {

let nodes = [];
let prompt: any = "once upon a time...";
for (let i = 0; i < 50; i++) {
for (let i = 0; i < 25; i++) {
const node = new ComputeText({ prompt });
nodes.push(node);
prompt = node.future.text.concat(" and then");
prompt = sb.concat(node.future.text, " and then");
}

const res = await substrate.run(...nodes);
Expand Down
22 changes: 0 additions & 22 deletions examples/qa.ts

This file was deleted.

53 changes: 42 additions & 11 deletions src/Future.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { idGenerator } from "substrate/idGenerator";
import { Node } from "substrate/Node";
import { unproxy, proxyFactory } from "./ProxiedFuture";

type Accessor = "item" | "attr";
type TraceOperation = {
Expand All @@ -8,7 +9,7 @@ type TraceOperation = {
accessor: Accessor;
};

type TraceProp = string | Future<string> | number | Future<number>;
export type TraceProp = string | Future<string> | number | Future<number>;
type Concatable = string | Future<string>;
type JQCompatible = Record<string, unknown> | any[] | string | number;
type JQDirectiveTarget = Future<any> | JQCompatible;
Expand All @@ -33,7 +34,11 @@ abstract class Directive {
// @ts-ignore
return this.items
.filter((p) => p instanceof Future)
.flatMap((p) => [p, ...p.referencedFutures()]);
.map((p) => unproxy(p) as Future<any>)
.flatMap((p) =>
// @ts-ignore
[p, ...p.referencedFutures()]
);
}
}

Expand Down Expand Up @@ -92,6 +97,11 @@ export class Trace extends Directive {
// @ts-expect-error (accessing protected prop: _id)
return Trace.Operation.future("attr", item._id);
} else if (typeof item === "string") {
if (/^\d+$/.test(item)) {
// When item is a number (as a string) we're going to assume it's an array index
let index = parseInt(item);
return Trace.Operation.key("item", index);
}
return Trace.Operation.key("attr", item);
}
return Trace.Operation.key("item", item);
Expand All @@ -117,7 +127,7 @@ export class JQ extends Directive {
rawValue: (val: JQCompatible) => ({ future_id: null, val }),
};

override next(...items: TraceProp[]) {
override next(..._items: TraceProp[]) {
return new JQ(this.query, this.target);
}

Expand Down Expand Up @@ -184,7 +194,7 @@ export class StringConcat extends Directive {
}
}

export class Future<T> {
export class Future<T = unknown> {
protected _directive: Directive;
protected _id: string = "";
protected _runtimeHint:
Expand Down Expand Up @@ -212,6 +222,15 @@ export class Future<T> {
return this._directive.result();
}

[Symbol.toPrimitive]() {
// Because we would like to use `Future` values as accessors with bracket-notation on proxied `Future` values
// we need to ensure that when a `Future` instance is being converted into a primitive (as all values are when
// used with bracket-access are) we track a reference to the value and use a special ID that can be used
// later on in the proxy to look up and use the original `Future` when constructing the `Trace` used in the
// resulting proxied `Future`.
return proxyFactory.futureToPrimitive(this);
}

toJSON() {
return {
id: this._id,
Expand All @@ -228,7 +247,8 @@ export class Future<T> {
* let newFuture = concat("string", node.future.someString, "!")
*/
export const concat = (...items: (string | Future<string>)[]) => {
return new Future<string>(new StringConcat(items));
const uitems = items.map((item) => item instanceof Future ? unproxy(item) : item);
return new Future<string>(new StringConcat(uitems));
};

/**
Expand All @@ -245,7 +265,13 @@ export const interpolate = (
return concat(
...strings.flatMap((s: string, i: number) => {
const expr = exprs[i];
return expr ? [s, expr instanceof Future ? expr : expr.toString()] : [s];
if (expr instanceof Future) {
return [s, unproxy(expr)];
} else if (!expr) {
return [s];
} else {
return [s, expr.toString()];
}
}),
);
};
Expand Down Expand Up @@ -279,15 +305,17 @@ export const get = <T = any>(
future: Future<Object>,
path: string | Future<string>,
) => {
const ufuture = unproxy(future);
const upath = path instanceof Future ? unproxy(path) : path;
// @ts-ignore (protected _runtimeHint)
if (path instanceof Future) index._runtimeHint = "string";
if (upath instanceof Future) upath._runtimeHint = "string";

const d =
typeof path === "string"
typeof upath === "string"
? // @ts-ignore (protected _directive)
future._directive.next(...parsePath(path))
ufuture._directive.next(...parsePath(upath))
: // @ts-ignore (protected _directive)
future._directive.next(path);
ufuture._directive.next(upath);
return new Future<T>(d);
};

Expand All @@ -299,7 +327,10 @@ export const get = <T = any>(
* let newFuture = at<string>(node.future.strings, 0);
*
*/
export const at = <T = any>(future: Future<T[]>, index: number | Future<number>) => {
export const at = <T = any>(
future: Future<T[]>,
index: number | Future<number>,
) => {
// @ts-ignore (protected _runtimeHint)
if (index instanceof Future) index._runtimeHint = "number";
// @ts-ignore (protected _directive)
Expand Down
2 changes: 1 addition & 1 deletion src/GEN_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20240617.20240711
20240617.20240715
13 changes: 9 additions & 4 deletions src/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Future, Trace } from "substrate/Future";
import { SubstrateResponse } from "substrate/SubstrateResponse";
import { NodeError, SubstrateError } from "substrate/Error";
import { AnyNode } from "substrate/Nodes";
import { unproxy } from "substrate/ProxiedFuture";

const generator = idGenerator("node");

Expand Down Expand Up @@ -109,7 +110,7 @@ export abstract class Node {

if (obj instanceof Future) {
// @ts-expect-error (accessing protected method toPlaceholder)
return obj.toPlaceholder();
return unproxy(obj).toPlaceholder();
}

if (obj && typeof obj === "object") {
Expand Down Expand Up @@ -161,11 +162,15 @@ export abstract class Node {
}

if (obj instanceof Future) {
futures.add(obj);
// Because some Future instances may be proxied, we need to "un-proxy"
// them to avoid treating it's methods as property accessors.
const future = unproxy(obj);

futures.add(future);

// @ts-expect-error (accessing protected method referencedFutures)
for (let future of obj.referencedFutures()) {
futures.add(future);
for (let f of future.referencedFutures()) {
futures.add(f);
}
return;
}
Expand Down
Loading