Skip to content
Closed
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
79 changes: 48 additions & 31 deletions apps/petrinaut-website/scripts/optimization-dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ const run = (command, args, options = {}) =>
});
});

const isOptimizerHealthy = async () => {
try {
const response = await fetch(`${optimizerOrigin}/status`);
return response.ok;
} catch {
return false;
}
};

const waitForOptimizer = async () => {
for (let attempt = 0; attempt < 60; attempt += 1) {
try {
Expand Down Expand Up @@ -65,43 +74,51 @@ const stopContainer = async () => {
};

try {
await run("docker", ["info"], { stdio: "ignore" }).catch(() => {
throw new Error(
"Docker is not running. Start Docker Desktop and run the command again.",
);
});
// An optimizer already serving on the port (e.g. the compose stack's) is
// reused as-is; starting a second container would fail on the port bind.
if (await isOptimizerHealthy()) {
console.log(`Reusing the optimizer already serving on ${optimizerOrigin}.`);
} else {
await run("docker", ["info"], { stdio: "ignore" }).catch(() => {
throw new Error(
"Docker is not running. Start Docker Desktop and run the command again.",
);
});

console.log("Building Petrinaut Opt...");
await run("docker", [
"build",
"--file",
"apps/petrinaut-opt/docker/Dockerfile",
"--tag",
image,
".",
]);

console.log("Starting Petrinaut Opt on http://127.0.0.1:4004...");
await run("docker", [
"run",
"--detach",
"--init",
"--read-only",
"--rm",
"--name",
container,
"--publish",
"127.0.0.1:4004:4004",
image,
]);
containerStarted = true;
await waitForOptimizer();
console.log("Building Petrinaut Opt...");
await run("docker", [
"build",
"--file",
"apps/petrinaut-opt/docker/Dockerfile",
"--tag",
image,
".",
]);

console.log("Starting Petrinaut Opt on http://127.0.0.1:4004...");
await run("docker", [
"run",
"--detach",
"--init",
"--read-only",
"--rm",
"--name",
container,
"--publish",
"127.0.0.1:4004:4004",
image,
]);
containerStarted = true;
await waitForOptimizer();
}

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

console.log("Starting the Petrinaut optimization demo...");
websiteProcess = spawn("yarn", ["vite"], {
// 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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
title: Running the loop locally
description: One command that stands up the whole optimization loop β€” browser, Python service, and CLI subprocess β€” without HASH.
sidebar_order: 20
attachTo: optimizer
---

The full optimization loop runs locally without any HASH infrastructure. From
the repository root:

```sh
yarn dev:petrinaut-optimization
```

Then open [http://localhost:5173/optimization](http://localhost:5173/optimization)
(or whatever port Vite prints). Simulate mode gains the **Optimizations** view,
and studies created there run against the real
[optimizer](layer:optimizer) service.

The command is `apps/petrinaut-website/scripts/optimization-dev.mjs`, and it
does five things in order:

1. Builds the `petrinaut-opt:local` Docker image from
`apps/petrinaut-opt/docker/Dockerfile`. The image bundles the
[CLI](layer:cli) at `/usr/local/bin/petrinaut`, so the service finds its
child executable without any checkout-specific setup.
2. Runs the container read-only on `127.0.0.1:4004` and polls its `/status`
endpoint until healthy (30 seconds, then it gives up).
3. Builds `@hashintel/petrinaut` through Turborepo β€” the demo website consumes
the **built dists**, not the sources.
4. Starts the website's Vite dev server with
`VITE_PETRINAUT_OPT_PROVIDER=service` and
`PETRINAUT_OPT_ORIGIN=http://127.0.0.1:4004`.
5. Stops and removes the container when you stop the command.

Docker must be running; nothing else is required. An optimizer already
serving on `127.0.0.1:4004` (the compose stack's, for example) is reused
instead of starting a second container β€” note a long-lived one can be too
old for the current protocol. Extra arguments are forwarded to Vite, so
`yarn dev:petrinaut-optimization --port 5175 --strictPort` pins the website
port for tooling that needs to know it.

## Why plain `yarn dev` shows no Optimizations view

The editor renders the Optimizations tab only when a
`PetrinautOptimizationContext` is mounted, and the website mounts one only on
the `/optimization` route with `VITE_PETRINAUT_OPT_PROVIDER=service` set. A
plain `turbo run dev` in `@apps/petrinaut-website` leaves the context null:
the tab is hidden and nothing optimization-related is reachable. Storybook
provides a fake optimizer for isolated UI work on the drawers.

## The request path

The browser never talks to the Python service directly. Vite's dev server
proxies `/api/petrinaut-opt/*` to the optimizer origin (rewriting the prefix
away), which avoids development-only CORS changes to the service. Behind
that, the service spawns one `petrinaut serve` subprocess per optimization
run and speaks JSON lines to it β€” the
[subprocess boundary](doc:optimizer/subprocess-boundary) covers that
contract, and the [CLI usage manual](doc:cli/usage-manual) the protocol.

```
browser ── /api/petrinaut-opt/* ──▢ Vite proxy ──▢ petrinaut-opt (127.0.0.1:4004)
β”‚ one per run
β–Ό
petrinaut serve (JSON lines)
```

Override the proxy target with `PETRINAUT_OPT_ORIGIN` when the service runs
somewhere other than `127.0.0.1:4004`.

## Without Docker

When iterating on the Python service itself, a container rebuild per change
is the wrong loop. Run the service directly:

```sh
cd apps/petrinaut-opt
uv sync
uv run uvicorn src.optimization_api:app --reload --port 4004
```

Two things the Docker image otherwise provides become your problem:

- **The CLI on `PATH`.** The service launches its child as `petrinaut` on a
fixed `PATH` (`/usr/local/bin:/usr/bin:/bin`). In a checkout, build it
(`turbo run build --filter @hashintel/petrinaut-cli`) and link its `bin`
onto that path β€” the [Python bindings manual](doc:python-bindings/usage-manual)
covers the options.
- **The port.** Bare `uvicorn --reload` defaults to 8000; pass `--port 4004`
or point the website at it with `PETRINAUT_OPT_ORIGIN`.

Then start the website side alone, with the provider enabled:

```sh
cd apps/petrinaut-website
VITE_PETRINAUT_OPT_PROVIDER=service yarn dev
```

Use the one-command Docker flow when you are working on Petrinaut and just
need a real optimizer behind it; use the uvicorn flow when the service is
what you are changing.
Loading