Skip to content

Commit

Permalink
21744: Enhanced Trainee object to expose Trainee related operations d…
Browse files Browse the repository at this point in the history
…irectly, MAJOR (#23)

- Added Trainee operation methods to the Trainee class
- Fixed comments becoming all lowercase

---------

Co-authored-by: Lance Gliser <lance.gliser@howso.com>
  • Loading branch information
fulpm and lancegliser authored Oct 21, 2024
1 parent 4b4cb06 commit b54c53e
Show file tree
Hide file tree
Showing 149 changed files with 3,673 additions and 2,829 deletions.
26 changes: 21 additions & 5 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@

This major change refactors the client and types for all Howso Engine operations.

### Typing Changes
### Typing changes

- Most of all the types are now autogenerated from the Engine API, and will have a different naming schema across
the board. However, most of the type's properties should remain the same.
- `types/shims` have been added as a stop gap for types lost from this migration. These will be transitioned out as
types improve in the Howso Engine API specification.
- The existing `Trainee` type has been renamed to `BaseTrainee` and no longer has a `features` property. Request
features via the method `getFeatureAttributes` instead.

### Client Changes
### Client changes

- `BaseClient` has been renamed to `AbstractBaseClient`.
- `WasmClient` has been renamed to `HowsoWorkerClient`.
Expand All @@ -23,15 +21,33 @@ This major change refactors the client and types for all Howso Engine operations
- The client's `ClientOptions.libDir` property has been removed.
- The client constructor now expects an instance of an `AbstractFileSystem` as its second parameter.
A `BrowserFileSystem` and `NodeFileSystem` are provided for use in their respective environments.
- The `train` method no longer batches requests to the Amalgam worker service automatically. Use `batchTrain` instead.
- The `createTrainee` method no longer sets feature attributes. Call `setFeatureAttributes` manually instead
(this also returns the updated object back).
- The `Trainee` object returned from the client now provides all Trainee operation methods directly. Such as train, react, etc.
- The `client/capabilities/*` interfaces have been removed.
- The return value of all Trainee operations has been changed to be an object with properties:
`payload` (this matches the previous return value format) and `warnings` (a list of warning messages, if applicable).
- The `train` method no longer batches requests to the Amalgam worker service automatically. Use `Trainee.batchTrain` instead.
- The `react` method now uses `context_values` instead of `context` and `action_values` instead of `actions`.
- `local_*` react features have been removed. Use their unqualified versions instead.

### Utilities changes

- The options of `inferFeatureAttributes`'s `service.infer`'s `InferFeatureAttributesOptions` have been updated to
a subset of standard [engine options](https://docs.howso.com/en/release-latest/api_reference/_autosummary/howso.utilities.html#howso.utilities.infer_feature_attributes). Please remap to the following:

```ts
type InferFeatureAttributesOptions = {
dependent_features?: Record<string, string[]>;
features?: FeatureAttributesIndex;
include_sample?: boolean;
infer_bounds?: boolean;
mode_bound_features?: string[];
ordinal_feature_values?: Record<string, string[]>;
tight_bounds?: boolean;
};
```

## 5.x

The `inferFeatureAttributes` function now requires a `sourceFormat` argument, and is strongly typed through a union.
Expand Down
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,42 @@ import { type ClientOptions, HowsoWorkerClient, BrowserFileSystem } from "@howso
const getClient = async (options?: ClientOptions): Promise<HowsoWorkerClient> => {
const worker = new Worker(new URL("@/workers/AmalgamWorker", import.meta.url), { type: "module" });
const fs = new BrowserFileSystem(worker);
const client = new HowsoWorkerClient(worker, fs, {
return await HowsoWorkerClient.create(worker, fs, {
howsoUrl,
migrationsUrl, // Optional, used for upgrading Trainees saved to disk.
...options,
});
return client.setup();
};
```

Once you have a client you can then start by creating a Trainee with some initial features and data:

```ts
const client: HowsoWorkerClient = await getClient();
const trainee = await client.createTrainee({ name: "MyTrainee" });
await trainee.setFeatureAttributes({ feature_attributes });
await trainee.batchTrain({ cases: dataset.data, columns: dataset.columns });
await trainee.analyze();
const { payload, warnings } = await trainee.react({
context_values: [
[1, 2],
[3, 4],
],
context_features: ["a", "b"],
action_features: ["target"],
});
```

Or loading a trained trainee via an existing `.caml` file:

```ts
import uri from "@/src/trainees/MyTrainee.caml?url";

const options = { id: "MyTrainee", uri };
await client.acquireTraineeResources(options.id, options.uri);
const trainee = await client.getTrainee(options.id);
```

## Publishing

Documentation changes do not require a version publishing.
Expand Down
74 changes: 57 additions & 17 deletions codegen/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import fs from "node:fs";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);

export const PRIMITIVE_TYPES = ["any", "boolean", "number", "string", "null"];
export type SchemaTypeOption = "any" | "assoc" | "boolean" | "list" | "number" | "string" | "null";
export type SchemaType = SchemaTypeOption | SchemaTypeOption[];
export type TypeDefinition = SchemaType | Schema | Ref | AnyOf;

export interface BaseSchema {
description?: string;
description?: string | null;
required?: boolean;
default?: any;
}
Expand All @@ -16,6 +18,10 @@ export interface Ref extends BaseSchema {
ref: string;
}

export interface AnyOf extends BaseSchema {
any_of: TypeDefinition[];
}

export interface Schema extends BaseSchema {
type: SchemaType;
enum?: (number | string)[];
Expand All @@ -25,17 +31,21 @@ export interface Schema extends BaseSchema {
exclusive_max?: number;
min_size?: number;
max_size?: number;
values?: SchemaType | Schema | Ref;
indices?: Record<string, SchemaType | Schema | Ref>;
dynamic_indices?: Record<string, SchemaType | Schema | Ref>;
additional_indices?: SchemaType | Schema | Ref | false;
values?: TypeDefinition;
min_indices?: number;
max_indices?: number;
indices?: Record<string, TypeDefinition>;
dynamic_indices?: Record<string, TypeDefinition>;
additional_indices?: TypeDefinition | false;
}

export interface LabelDefinition {
parameters: Record<string, Schema | Ref> | null;
returns?: SchemaType | Schema | Ref | null;
parameters: Record<string, Exclude<TypeDefinition, SchemaType>> | null;
returns?: TypeDefinition | null;
description?: string | null;
attribute?: boolean;
use_active_session?: boolean;
attribute?: SchemaType | null;
payload?: boolean;
long_running?: boolean;
read_only?: boolean;
idempotent?: boolean;
Expand All @@ -44,7 +54,7 @@ export interface LabelDefinition {

export interface EngineApi {
readonly labels: Record<string, LabelDefinition>;
readonly schemas: Record<string, Schema>;
readonly schemas: Record<string, Exclude<TypeDefinition, SchemaType>>;
readonly description: string;
readonly version: string;
}
Expand Down Expand Up @@ -72,8 +82,9 @@ export async function getEngineApi(): Promise<EngineApi> {

try {
// Load the Howso Engine into Amalgam
amalgam.runtime.FS.writeFile("howso.caml", fs.readFileSync(enginePath));
amalgam.loadEntity(handle, "howso.caml");
const filePath = "howso.caml";
amalgam.runtime.FS.writeFile(filePath, fs.readFileSync(enginePath));
amalgam.loadEntity({ handle, filePath });
console.log(`Amalgam Version: ${amalgam.getVersion()}`);

// Initialize the Engine
Expand All @@ -83,7 +94,7 @@ export async function getEngineApi(): Promise<EngineApi> {
}

// Get the api documentation from the Engine
const response = amalgam.executeEntityJson(handle, "get_api", "");
const response = amalgam.executeEntityJson(handle, "get_api", {});
if (!Array.isArray(response) || response[0] != 1) {
throw new Error("Failed to retrieve API documentation from the Howso Engine.");
}
Expand All @@ -95,21 +106,50 @@ export async function getEngineApi(): Promise<EngineApi> {
}
}

export function isRef(value: SchemaType | Schema | Ref | null | undefined): value is Ref {
/** Check if a type is a AnyOf object. */
export function isAnyOf(value: TypeDefinition | null | undefined): value is AnyOf {
if (value == null || Array.isArray(value) || typeof value === "string") {
return false;
}
return "any_of" in value && Array.isArray(value.any_of) && value.any_of.length > 0;
}

/** Check if a type is a Ref object. */
export function isRef(value: TypeDefinition | null | undefined): value is Ref {
if (value == null || Array.isArray(value) || typeof value === "string") {
return false;
}
return "ref" in value && value.ref != null;
}

export function isSchema(value: SchemaType | Schema | Ref | null | undefined): value is Schema {
/** Check if a type is a Schema object. */
export function isSchema(value: TypeDefinition | null | undefined): value is Schema {
if (value == null || Array.isArray(value) || typeof value === "string") {
return false;
}
return !isRef(value) && "type" in value && (typeof value.type === "string" || Array.isArray(value.type));
}

export function isSchemaOrRef(value: SchemaType | Schema | Ref | boolean | null | undefined): value is Schema | Ref {
if (typeof value === "boolean") return false;
return isRef(value) || isSchema(value);
/** Check if a type is a Schema, Ref, or AnyOf object. */
export function isAnySchema(value: TypeDefinition | boolean | null | undefined): value is Schema | Ref {
if (value == null || typeof value === "boolean") return false;
return isRef(value) || isSchema(value) || isAnyOf(value);
}

/** Check if a type is a primitive or simple array. */
export function isSimpleType(value: any) {
if (value == null) return false;
if (typeof value === "string") {
return PRIMITIVE_TYPES.includes(value);
} else if (Array.isArray(value)) {
return value.map((v) => PRIMITIVE_TYPES.includes(v)).every(Boolean);
} else if (isRef(value)) {
return true;
} else if (isSchema(value)) {
if (value.type === "list") {
return isSimpleType(value.values);
}
return isSimpleType(value.type);
}
return false;
}
7 changes: 5 additions & 2 deletions codegen/filters/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
*/
export function blockComment(content: any) {
// Ensure the content is a string
const str = typeof content === "string" ? content : String(content);
let str = typeof content === "string" ? content : String(content);

return str
str = str
.split("\n")
.reduceRight<string[]>((accumulator, value) => {
// Removing tailing empty lines
Expand All @@ -34,4 +34,7 @@ export function blockComment(content: any) {
})
.join("\n")
.trimEnd();

// Capitalize first sentence.
return str.charAt(0).toUpperCase() + str.slice(1);
}
6 changes: 4 additions & 2 deletions codegen/filters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ export function registerFilters(env: Environment) {
env.addFilter(strings.camelCase.name, strings.camelCase);
env.addFilter(strings.toJson.name, strings.toJson);
env.addFilter(strings.autoQuote.name, strings.autoQuote);
env.addFilter(types.isString.name, types.isString);
env.addFilter(types.isArray.name, types.isArray);
env.addFilter(types.enumMatchesType.name, types.enumMatchesType);
env.addFilter(types.convertType.name, types.convertType);
env.addFilter(engine.isRef.name, engine.isRef);
env.addFilter(engine.isAnyOf.name, engine.isAnyOf);
env.addFilter(engine.isSchema.name, engine.isSchema);
env.addFilter(engine.isSchemaOrRef.name, engine.isSchemaOrRef);
env.addFilter(engine.isAnySchema.name, engine.isAnySchema);
env.addFilter(engine.isSimpleType.name, engine.isSimpleType);
}
21 changes: 15 additions & 6 deletions codegen/filters/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { SchemaType } from "../engine";

export function isString(value: any) {
return typeof value === "string";
}

/** Check if value is an Array. */
export function isArray(value: any) {
return Array.isArray(value);
}

/**
* Check if an enum contains at least one value of the specified type.
*
* This is required for properties that support both string and number values and defines an enum. When rendering the
* enum values we check if the enum contains any values for that type and if not we render just the type itself,
* otherwise we enumerate the valid options.
*/
export function enumMatchesType(enumeration: Array<string | number> | null, type: "number" | "string") {
if (!Array.isArray(enumeration)) return false;
return enumeration.some((value) => typeof value === type);
}

/** Convert Amalgam type to TypeScript type. */
export function convertType(schema: SchemaType) {
let value: string;
switch (schema) {
Expand All @@ -33,8 +43,7 @@ export function convertType(schema: SchemaType) {
value = "any";
break;
default:
console.warn(`Unexpected type received: ${schema}`);
value = "any";
throw new Error(`Unexpected Amalgam type received ${JSON.stringify(schema)}`);
}

return this.env.filters.safe(value);
Expand Down
Loading

0 comments on commit b54c53e

Please sign in to comment.