diff --git a/apps/petrinaut-website/scripts/optimization-dev.mjs b/apps/petrinaut-website/scripts/optimization-dev.mjs index 903336115ad..59b40f27d00 100644 --- a/apps/petrinaut-website/scripts/optimization-dev.mjs +++ b/apps/petrinaut-website/scripts/optimization-dev.mjs @@ -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 { @@ -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, diff --git a/libs/@local/petrinaut-arch-docs/content/optimizer/running-the-loop-locally.mdx b/libs/@local/petrinaut-arch-docs/content/optimizer/running-the-loop-locally.mdx new file mode 100644 index 00000000000..26051bfabe0 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/optimizer/running-the-loop-locally.mdx @@ -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.