Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/storybook-real-optimizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut": patch
---

Storybook gains a "With real optimizer" story: the full editor, built from source with fast refresh, running optimization studies against a local Petrinaut Optimizer service. Start it with `yarn dev:petrinaut-optimization --storybook`.
2 changes: 1 addition & 1 deletion apps/petrinaut-website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"lint:eslint": "oxlint --type-aware --report-unused-disable-directives-severity=error .",
"lint:tsc": "tsgo --noEmit",
"preview": "vite preview",
"test:unit": "vitest run"
"test:unit": "vitest run --passWithNoTests"
},
"dependencies": {
"@ai-sdk/openai": "3.0.63",
Expand Down
58 changes: 43 additions & 15 deletions apps/petrinaut-website/scripts/optimization-dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ const container = "petrinaut-opt-website-dev";
// nosemgrep: typescript.react.security.react-insecure-request.react-insecure-request
const optimizerOrigin = "http://127.0.0.1:4004";

// `--storybook` starts Petrinaut's Storybook (editor built from source, with
// fast refresh) against the optimizer instead of the demo website (which
// consumes the built dist). Remaining arguments go to the spawned dev server.
const cliArguments = process.argv.slice(2);
const storybookMode = cliArguments.includes("--storybook");
const forwardedArguments = cliArguments.filter(
(argument) => argument !== "--storybook",
);

const wait = (durationMs) =>
new Promise((resolve) => setTimeout(resolve, durationMs));

Expand Down Expand Up @@ -166,21 +175,40 @@ try {
await waitForOptimizer();
}

console.log("Building Petrinaut for the demo website...");
await run("turbo", ["build", "--filter", "@hashintel/petrinaut"]);

console.log("Starting the Petrinaut optimization demo...");
// Extra arguments go to Vite, so a caller can pin the port:
// `yarn dev:petrinaut-optimization --port 5175 --strictPort`.
websiteProcess = spawn("yarn", ["vite", ...process.argv.slice(2)], {
cwd: appDirectory,
env: {
...process.env,
PETRINAUT_OPT_ORIGIN: optimizerOrigin,
VITE_PETRINAUT_OPT_PROVIDER: "service",
},
stdio: "inherit",
});
const providerEnv = {
...process.env,
PETRINAUT_OPT_ORIGIN: optimizerOrigin,
VITE_PETRINAUT_OPT_PROVIDER: "service",
};

if (storybookMode) {
console.log("Starting Petrinaut's Storybook against the optimizer...");
// Through Turborepo so Storybook's workspace dependencies are built;
// Storybook itself serves the editor from source with fast refresh.
websiteProcess = spawn(
"turbo",
[
"run",
"dev",
"--filter",
"@hashintel/petrinaut",
...(forwardedArguments.length > 0 ? ["--", ...forwardedArguments] : []),
],
{ cwd: repositoryRoot, env: providerEnv, stdio: "inherit" },
);
} else {
console.log("Building Petrinaut for the demo website...");
await run("turbo", ["build", "--filter", "@hashintel/petrinaut"]);

console.log("Starting the Petrinaut optimization demo...");
// Extra arguments go to Vite, so a caller can pin the port:
// `yarn dev:petrinaut-optimization --port 5175 --strictPort`.
websiteProcess = spawn("yarn", ["vite", ...forwardedArguments], {
cwd: appDirectory,
env: providerEnv,
stdio: "inherit",
});
}

const forwardSignal = (signal) => websiteProcess?.kill(signal);
const handleSigint = () => forwardSignal("SIGINT");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import {
attachPetrinautOptimizationRunStream,
createPetrinautOptimizerClient,
PetrinautOptimizerHttpError,
petrinautOptimizerHttpErrorFromResponse,
} from "@local/petrinaut-optimizer-client";
import { createServicePetrinautOptimization } from "@local/petrinaut-optimizer-client";

import type {
PetrinautOptimization,
PetrinautOptimizationEvent,
} from "@hashintel/petrinaut-core";
import type { PetrinautOptimization } from "@hashintel/petrinaut-core";
import type { PetrinautOptimizerFetch } from "@local/petrinaut-optimizer-client";

/**
Expand All @@ -23,103 +15,11 @@ const petrinautOptEndpoint = (): URL =>
typeof location === "undefined" ? "http://localhost/" : location.href,
);

/**
* Stamp the duck-typed classification fields Petrinaut's optimization
* provider reads (`category`, `httpStatus`, `retryAfter`) onto the client's
* HTTP error, so e.g. a 404 on re-attaching to an expired run silently drops
* the record instead of surfacing a raw error message.
*/
const classifyHttpError = (error: unknown): unknown =>
error instanceof PetrinautOptimizerHttpError
? Object.assign(error, {
category: "http",
httpStatus: error.status,
...(error.retryAfter === null
? {}
: { retryAfter: Number.parseInt(error.retryAfter, 10) }),
})
: error;

/**
* Classify a mid-stream failure so the provider reconnects with its cursor
* instead of failing the run on the first dropped connection. Aborts pass
* through untouched. A response body that dies mid-stream rejects the reader
* with a `TypeError`, which is a transport failure rather than a malformed
* frame β€” the remaining non-abort errors are the decoder's own validation
* failures, which stay `protocol`.
*/
const classifyStreamError = (error: unknown): unknown =>
error instanceof Error && error.name !== "AbortError"
? Object.assign(error, {
category: error instanceof TypeError ? "network" : "protocol",
})
: error;

/**
* Classify a request-time failure: HTTP errors keep their status semantics,
* and anything else non-abort (a fetch `TypeError` from a dropped
* connection) is `network` β€” so an attach that dies before responding
* reconnects with backoff exactly like a mid-stream drop, instead of
* definitively failing a possibly-live run.
*/
const classifyRequestError = (error: unknown): unknown =>
error instanceof PetrinautOptimizerHttpError
? classifyHttpError(error)
: error instanceof Error && error.name !== "AbortError"
? Object.assign(error, { category: "network" })
: error;

/** Create the local-only Petrinaut capability backed directly by Python. */
export const createPetrinautOptOptimization = (
fetchImpl: PetrinautOptimizerFetch = fetch,
): PetrinautOptimization => {
const client = createPetrinautOptimizerClient(
petrinautOptEndpoint(),
): PetrinautOptimization =>
createServicePetrinautOptimization({
endpoint: petrinautOptEndpoint,
fetchImpl,
);
// openapi-fetch names its verb methods in caps; alias them so call sites
// don't read as constructor calls (oxlint's new-cap).
const { DELETE: deleteRun, POST: postRun } = client;

return {
async createOptimizationRun(input, options) {
const created = await postRun("/optimize/runs", {
body: input,
...(options?.signal ? { signal: options.signal as AbortSignal } : {}),
}).catch((error: unknown) => {
throw classifyRequestError(error);
});
if (!created.response.ok || !created.data?.run_id) {
throw classifyHttpError(
await petrinautOptimizerHttpErrorFromResponse(created.response),
);
}
return { runId: created.data.run_id };
},
async *attachOptimizationRun(runId, options) {
let events: AsyncIterable<PetrinautOptimizationEvent>;
try {
({ events } = await attachPetrinautOptimizationRunStream({
endpoint: petrinautOptEndpoint(),
fetchImpl,
runId,
...(options?.cursor === undefined ? {} : { cursor: options.cursor }),
...(options?.signal ? { signal: options.signal } : {}),
}));
} catch (error) {
throw classifyRequestError(error);
}
options?.onAttached?.();
try {
yield* events;
} catch (error) {
throw classifyStreamError(error);
}
},
async cancelOptimizationRun(runId) {
await deleteRun("/optimize/runs/{run_id}", {
params: { path: { run_id: runId } },
});
},
};
};
});
85 changes: 65 additions & 20 deletions libs/@hashintel/petrinaut/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,21 @@
"browser": true
},
"rules": {
"array-callback-return": ["error", { "allowImplicit": true }],
"array-callback-return": [
"error",
{
"allowImplicit": true
}
],
"default-case-last": "error",
"default-param-last": "error",
"eqeqeq": ["error", "always", { "null": "ignore" }],
"eqeqeq": [
"error",
"always",
{
"null": "ignore"
}
],
"guard-for-in": "error",
"no-alert": "error",
"no-cond-assign": ["error", "always"],
Expand All @@ -34,7 +45,9 @@
"no-template-curly-in-string": "error",
"no-unsafe-optional-chaining": [
"error",
{ "disallowArithmeticOperators": true }
{
"disallowArithmeticOperators": true
}
],
"no-unused-vars": [
"error",
Expand All @@ -44,27 +57,35 @@
"varsIgnorePattern": "^_+"
}
],
"no-void": ["error", { "allowAsStatement": true }],

"no-void": [
"error",
{
"allowAsStatement": true
}
],
"no-console": "error",
"new-cap": "error",
"no-new-func": "error",
"func-names": "error",
"no-bitwise": "error",
"no-multi-assign": "error",

"no-restricted-globals": [
"error",
{ "name": "isFinite", "message": "Use Number.isFinite instead" },
{ "name": "isNaN", "message": "Use Number.isNaN instead" },
{
"name": "isFinite",
"message": "Use Number.isFinite instead"
},
{
"name": "isNaN",
"message": "Use Number.isNaN instead"
},
"event",
"name",
"length",
"status"
],
"no-shadow": "error",
"no-use-before-define": "error",

"no-restricted-imports": [
"error",
{
Expand All @@ -76,23 +97,35 @@
]
}
],

"import/no-named-as-default": "error",
"import/no-named-as-default-member": "error",
"import/no-mutable-exports": "error",
"import/no-duplicates": "error",
"import/no-named-default": "error",
"import/no-self-import": "error",
"import/no-cycle": "error",

"react/jsx-pascal-case": ["error", { "allowAllCaps": true }],
"react/jsx-pascal-case": [
"error",
{
"allowAllCaps": true
}
],
"react/no-danger": "error",
"react/jsx-no-target-blank": ["error", { "enforceDynamicLinks": "always" }],
"react/jsx-no-target-blank": [
"error",
{
"enforceDynamicLinks": "always"
}
],
"react/jsx-no-comment-textnodes": "error",
"react/no-array-index-key": "error",
"react/button-has-type": [
"error",
{ "button": true, "submit": true, "reset": false }
{
"button": true,
"submit": true,
"reset": false
}
],
"react-hooks-js/static-components": "error",
"react-hooks-js/use-memo": "error",
Expand All @@ -110,12 +143,19 @@
"react-hooks-js/unsupported-syntax": "error",
"react-hooks-js/config": "error",
"react-hooks-js/gating": "error",

"jsx-a11y/prefer-tag-over-role": "off",
"jsx-a11y/aria-role": ["error", { "ignoreNonDOM": false }],
"jsx-a11y/aria-role": [
"error",
{
"ignoreNonDOM": false
}
],
"jsx-a11y/no-noninteractive-tabindex": [
"error",
{ "tags": [], "roles": ["tabpanel"] }
{
"tags": [],
"roles": ["tabpanel"]
}
],
"jsx-a11y/label-has-associated-control": "error",
"jsx-a11y/no-static-element-interactions": [
Expand All @@ -131,7 +171,6 @@
]
}
],

"@typescript-eslint/ban-ts-comment": [
"error",
{
Expand All @@ -149,10 +188,8 @@
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-function-type": "error",

"unicorn/no-new-array": "off",
"unicorn/filename-case": "error",

"constructor-super": "off",
"no-class-assign": "off",
"no-const-assign": "off",
Expand All @@ -175,5 +212,13 @@
"*.gen.*",
"*.tsbuildinfo",
".turbo/**"
],
"overrides": [
{
"files": ["src/**/*.stories.tsx", ".storybook/**/*.{ts,tsx}"],
"rules": {
"no-restricted-imports": "off"
}
}
]
}
Loading
Loading