Skip to content
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ lib64/
parts/
sdist/
var/
wheels/
# Anchored to the repo root so static/wheels/ (browser playground wheels) stays tracked
/wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
Expand Down
78 changes: 78 additions & 0 deletions BROWSER_PLAYGROUND_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Browser Playground

This document explains how executable code snippets run in the reader's browser and how to maintain the setup.

## What It Is

Executable docs snippets run directly in the reader's browser. There is no execution server. Python code runs on Pyodide, which is CPython compiled to WebAssembly, and TypeScript code is transpiled in the browser and executed as a native ES module. Pyodide is loaded from the jsDelivr CDN, pinned to version 0.29.4. The pin matters because dependencies with native code (pydantic-core, cryptography) only exist as WebAssembly builds inside the Pyodide distribution, so the bundled pydantic version must satisfy the client wheel's pydantic floor. Check the new `pyodide-lock.json` before bumping either side.

## How It Works

`src/theme/Tabs/pyodideRunner.js` lazy-loads Pyodide the first time a reader clicks **Run**. It then installs the two wheels from `static/wheels/` with micropip and runs the snippet with top-level `await` support. The interpreter is a module-level singleton that survives client-side page navigation, so imports, variables, and the connected client carry over between runs, even on different pages.

The two wheels must be installed in a single `micropip.install()` call. The grpc-web wheel declares an unpinned dependency on `weaviate-client`, and installing both together lets the local client wheel satisfy that dependency instead of pulling a release from PyPI.

## The TypeScript Client

TypeScript snippets are handled by `src/theme/Tabs/tsRunner.js`. It loads the TypeScript compiler from jsDelivr (pinned to 5.9.3) on the first Run click, transpiles the snippet, rewrites its `weaviate-client` import to the browser bundle served from `static/playground/weaviate-client.web.js`, and runs the result as a native ES module through a Blob URL. Each run is a fresh module, so unlike Python nothing persists between runs.

The bundle is built from typescript-client PR #437 (grpc-web transport, browser shims for Node built-ins). To rebuild it:

```bash
cd ~/dev/typescript-client && git fetch origin pull/437/head && git worktree add /tmp/tsc-437 FETCH_HEAD
cd /tmp/tsc-437 && npm ci && npm run build:web
cp dist/web/index.web.js <docs repo>/static/playground/weaviate-client.web.js
```

**Note:** The bundle from `typescript-client` contains bare imports (e.g., `import 'long'`) that browsers cannot resolve. You **must** run the following command in the docs repo to finalize the bundle (this requires `nice-grpc-web` to be installed in the docs repo):

```bash
yarn build:playground-client
```

The bundle's web entry (`connectToCustom`, `connectToWeaviateCloud`) speaks grpc-web on the main host with the default path prefix `/grpc-web`, matching the Python snippet's connection defaults.

## Reader Credentials

The **Connect** button next to **Run** stores the reader's Weaviate Cloud cluster URL and API key in `localStorage` (`src/theme/Tabs/playgroundCredentials.js`). Before each run the runner injects them as the `WEAVIATE_URL` (normalized to a bare host) and `WEAVIATE_API_KEY` environment variables — `os.environ` for Python, a `process.env` shim for TypeScript — and removes them when cleared. Snippets read them with `os.environ.get(...)` and fall back to `localhost` when unset. The values never leave the reader's browser except in the requests the snippet itself makes to their Weaviate instance.

Hitting **Run** with no stored connection details does not execute. Instead it opens the Connect panel in prompt mode, which explains that the snippet runs against the reader's own cluster, links to creating a free Weaviate Cloud cluster, and offers **Save and run** (starts the run once a cluster URL is saved) plus a **Use local instance** escape hatch for readers with a local grpc-web-fronted Weaviate. The local choice is remembered in `localStorage` (`weaviatePlayground.useLocalInstance`) so subsequent runs go straight through; clearing credentials also forgets it. This prompt is an interim flow — once docs can provision an ephemeral Weaviate Cloud cluster dynamically, it will be replaced by one-click provisioning.

## The Python Client

The wheels are built from weaviate-python-client PR #2056 (branch `feat/grpc-web-wasm-transport`). Only the async client works under WebAssembly, the sync client raises an error. gRPC calls are routed over grpc-web using `fetch`. The companion package also reroutes the client's REST calls (httpx) through `fetch`, since httpx normally opens raw sockets that do not exist in the browser.

Snippets must import `weaviate_grpc_web` before `weaviate`, then connect with `weaviate.use_async_with_custom(...)` using `grpc_path_prefix="/grpc-web"` (which enables grpc-web and lets REST and gRPC share a host and port) and `skip_init_checks=True`.

### Rebuilding the wheels

```bash
cd ~/dev/weaviate-python-client && git fetch origin feat/grpc-web-wasm-transport && git worktree add /tmp/wpc-grpcweb FETCH_HEAD
cd /tmp/wpc-grpcweb && SETUPTOOLS_SCM_PRETEND_VERSION=4.21.4.dev5 uv build --wheel --out-dir /tmp/wheels .
cd packages/grpc-web && uv build --wheel --out-dir /tmp/wheels .
cp /tmp/wheels/*.whl <docs repo>/static/wheels/
```

The wheel filenames (`weaviate_client-4.21.4.dev5-py3-none-any.whl` and `weaviate_python_grpc_web-0.0.1.dev0-py3-none-any.whl`) are referenced in `src/theme/Tabs/pyodideRunner.js`. If a rebuild changes a filename, update both together.

## Server Requirements

Snippets need a Weaviate instance that speaks grpc-web. As of Weaviate 1.38 (verified on `1.38.0-rc.1`), grpc-web support is built in (weaviate/weaviate#11673): set `GRPC_WEB_ENABLED=true` and an in-process transcoder serves grpc-web on the REST port under `/grpc-web/` — no Envoy or external proxy needed. The built-in transcoder sends CORS headers (`*` by default, configurable via `CORS_ALLOW_ORIGIN`) and exposes the `grpc-status`/`grpc-message` response headers. Older Weaviate versions need an external grpc-web transcoder such as Envoy or connectrpc/vanguard-go with equivalent CORS treatment.

The snippet defaults assume REST and grpc-web share `localhost:8080` with the path prefix `/grpc-web` (Weaviate Cloud clusters use port 443). The local test instance in `tests/docker-compose-anon.yml` has `GRPC_WEB_ENABLED` set, so the playground's **Use local instance** path works against it with no credentials. One quirk: Weaviate's regular REST routes (`/v1/meta`, `/v1/schema`, …) send `Access-Control-Allow-Origin`, but `/v1/.well-known/ready` does not (the liveness/readiness middleware short-circuits before the CORS middleware), so browsers block `is_ready()` specifically — snippets should skip it (the movies snippet keeps it commented out for this reason). Chrome and Firefox allow `http://localhost` fetches from an https page, so a local instance works from the production docs site in those browsers. Safari blocks them, and Chrome's Private Network Access checks may require answering a preflight request.

## Limitations

- Async client only, the sync client does not work under WebAssembly.
- No `batch.stream()` or `batch.experimental()`, bidirectional streaming is not possible over grpc-web.
- Java, C#, and Go snippets cannot run in the browser.
- TypeScript runs each snippet as a fresh module, so no state carries over between runs the way it does for Python.
- A run times out after 60 seconds. After a timeout the page keeps working but the old run may still hold the interpreter, so reload the page for a clean session.

## Validating Without a Browser

Pyodide also runs under Node, so the install path can be tested locally with `_build_scripts/browser-playground-smoke.mjs` (usage instructions are in the script header). It exercises the package preloads, the micropip install of the local wheels, the import chain, and client construction. Run it after rebuilding the wheels or bumping the Pyodide version. The preload list in the script must mirror the one in `src/theme/Tabs/pyodideRunner.js`.

## Validation Status

Verified end to end under WebAssembly (via Node, June 2026) against a Weaviate Cloud dev cluster fronted by a grpc-web transcoder: dependency resolution, wheel install, the import chain, `is_ready()` over REST through the fetch transport, collection listing over REST, and a gRPC-web query that round-trips a clean server error. The only browser-specific leg Node cannot exercise is CORS, which the server must grant on both the grpc-web route and the REST passthrough.
66 changes: 66 additions & 0 deletions _build_scripts/browser-playground-smoke.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Smoke test for the browser playground (see BROWSER_PLAYGROUND_README.md).
//
// Pyodide also runs under Node, so this validates the exact install path the
// browser uses — package preloads, micropip resolution of the local wheels,
// and the weaviate_grpc_web + weaviate import chain — without a browser.
// It does NOT validate the network path (grpc-web transcoder, CORS).
//
// Usage:
// mkdir -p /tmp/pyodide-smoke && cd /tmp/pyodide-smoke
// npm install pyodide@<version pinned in src/theme/Tabs/pyodideRunner.js>
// node <docs repo>/_build_scripts/browser-playground-smoke.mjs
//
// The pyodide npm package is intentionally not a repo dependency, it caches
// ~30 MB of runtime wheels into node_modules on first run.

import { readFileSync, readdirSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import { createRequire } from "module";

// Resolve pyodide from the directory the script is RUN from (the scratch dir
// where it was npm-installed), not from this script's location in the repo
const { loadPyodide } = createRequire(join(process.cwd(), "noop.js"))("pyodide");

const WHEELS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "static", "wheels");

// Must mirror the loadPackage list in src/theme/Tabs/pyodideRunner.js
const PRELOADS = ["micropip", "pydantic", "ssl", "cryptography", "anyio"];

const wheels = readdirSync(WHEELS_DIR).filter((f) => f.endsWith(".whl"));
if (wheels.length === 0) {
throw new Error(`No wheels found in ${WHEELS_DIR}`);
}
console.log("wheels:", wheels.join(", "));

const py = await loadPyodide();
console.log("pyodide loaded:", py.runPython("import sys; sys.version"));

await py.loadPackage(PRELOADS);
console.log("preloads OK, pydantic:", py.runPython("import pydantic; pydantic.VERSION"));

// Node's fetch cannot load the http wheel URLs the browser uses, so write the
// wheels into the in-memory filesystem and install via emfs:
for (const w of wheels) {
py.FS.writeFile(`/tmp/${w}`, readFileSync(join(WHEELS_DIR, w)));
}
const micropip = py.pyimport("micropip");
await micropip.install(wheels.map((w) => `emfs:/tmp/${w}`));
console.log("micropip install OK");

const result = await py.runPythonAsync(`
import sys
assert sys.platform == "emscripten", sys.platform
import weaviate_grpc_web # must be imported before weaviate
import weaviate
from weaviate.classes.init import Auth

client = weaviate.use_async_with_custom(
http_host="localhost", http_port=8080, http_secure=False,
grpc_host="localhost", grpc_port=8080, grpc_secure=False,
grpc_path_prefix="/grpc-web", skip_init_checks=True,
)
f"OK: {type(client).__name__}, weaviate {weaviate.__version__}"
`);
console.log("RESULT:", result);
process.exit(0);
97 changes: 97 additions & 0 deletions _includes/code/python/quickstart.movies.browser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# EndToEndExample
import asyncio
import os

import weaviate_grpc_web # must be imported before weaviate, installs the grpc-web transport
import weaviate
from weaviate.classes.init import Auth
from weaviate.classes.config import Configure, Property, DataType
from weaviate.classes.query import MetadataQuery

# Step 1: Connect
# Set your credentials via the Connect button next to Run, or edit these values inline
wcd_host = (
os.environ.get("WEAVIATE_URL", "localhost")
.removeprefix("https://")
.removeprefix("http://")
.split("/")[0]
.split(":")[0]
)
wcd_api_key = os.environ.get("WEAVIATE_API_KEY", "")
is_local = wcd_host == "localhost"

client = weaviate.use_async_with_custom(
http_host=wcd_host,
http_port=8080 if is_local else 443,
http_secure=not is_local,
grpc_host=wcd_host,
grpc_port=8080 if is_local else 443,
grpc_secure=not is_local,
grpc_path_prefix="/grpc-web",
auth_credentials=Auth.api_key(wcd_api_key) if wcd_api_key else None,
skip_init_checks=True,
)
await client.connect()

# ready = await client.is_ready()
# print(f"Connected to Weaviate, ready: {ready}")

# Step 2: Create a collection (deleted first, so the example is safe to re-run)
await client.collections.delete("Movies")

movies = await client.collections.create(
name="Movies",
properties=[
Property(name="title", data_type=DataType.TEXT),
Property(name="description", data_type=DataType.TEXT),
Property(name="year", data_type=DataType.INT),
],
# highlight-start
vector_config=Configure.Vectors.text2vec_weaviate( # Weaviate Embeddings
base_url="https://dev-embedding.labs.weaviate.io",
),
# highlight-end
)
print(f"Created collection: {movies.name}")

# Step 3: Import data, vectorized server-side by Weaviate Embeddings
data = [
{"title": "The Matrix", "description": "A hacker discovers reality is a simulation and joins a rebellion against the machines.", "year": 1999},
{"title": "Inception", "description": "A thief who steals secrets through dream-sharing technology is given one final job, planting an idea.", "year": 2010},
{"title": "Interstellar", "description": "Explorers travel through a wormhole in space to ensure humanity's survival.", "year": 2014},
{"title": "The Godfather", "description": "The aging patriarch of a crime dynasty transfers control to his reluctant son.", "year": 1972},
{"title": "Spirited Away", "description": "A young girl wanders into a world of spirits and must work in a bathhouse to free her parents.", "year": 2001},
{"title": "Toy Story", "description": "A cowboy doll feels threatened when a new spaceman action figure becomes the favorite toy.", "year": 1995},
{"title": "Jaws", "description": "A giant great white shark terrorizes a small beach community.", "year": 1975},
{"title": "La La Land", "description": "A jazz pianist and an aspiring actress fall in love while chasing their dreams in Los Angeles.", "year": 2016},
{"title": "Mad Max: Fury Road", "description": "In a post-apocalyptic wasteland, a drifter and a rebel warrior flee a tyrant in an armored war rig.", "year": 2015},
{"title": "Finding Nemo", "description": "A timid clownfish crosses the ocean to rescue his son, who was captured by a diver.", "year": 2003},
]

result = await movies.data.insert_many(data)
if result.has_errors:
print(f"Import errors: {result.errors}")
else:
aggregate = await movies.aggregate.over_all(total_count=True)
print(f"Imported {aggregate.total_count} movies")

# Step 4: Semantic search
# The vector index updates in the background after import, so retry briefly
response = None
for _ in range(15):
# highlight-start
response = await movies.query.near_text(
query="a science fiction adventure in space",
limit=3,
return_metadata=MetadataQuery(distance=True),
)
# highlight-end
if response.objects:
break
await asyncio.sleep(1)

for obj in response.objects:
print(f"{obj.properties['title']} ({obj.properties['year']}) — distance {obj.metadata.distance:.3f}")

await client.close() # Free up resources
# END EndToEndExample
89 changes: 89 additions & 0 deletions _includes/code/typescript/quickstart.movies.browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// EndToEndExample
import weaviate, { dataType } from "weaviate-client";

// Step 1: Connect
// Set your credentials via the Connect button next to Run, or edit these values inline
const wcdHost = (process.env.WEAVIATE_URL || "localhost")
.replace(/^https?:\/\//, "")
.split("/")[0]
.split(":")[0];
const wcdApiKey = process.env.WEAVIATE_API_KEY || "";
const isLocal = wcdHost === "localhost";

const client = await weaviate.connectToCustom({
httpHost: wcdHost,
httpPort: isLocal ? 8080 : 443,
httpSecure: !isLocal,
grpcHost: wcdHost,
grpcPort: isLocal ? 8080 : 443,
grpcSecure: !isLocal,
authCredentials: wcdApiKey ? new weaviate.ApiKey(wcdApiKey) : undefined,
skipInitChecks: true,
});

// Step 2: Create a collection (deleted first, so the example is safe to re-run)
await client.collections.delete("Movies");

const movies = await client.collections.create({
name: "Movies",
properties: [
{ name: "title", dataType: dataType.TEXT },
{ name: "description", dataType: dataType.TEXT },
{ name: "year", dataType: dataType.INT },
],
// highlight-start
vectorizers: weaviate.configure.vectors.text2VecWeaviate({
// Weaviate Embeddings
baseURL: "https://dev-embedding.labs.weaviate.io",
}),
// highlight-end
});
console.log(`Created collection: ${movies.name}`);

// Step 3: Import data, vectorized server-side by Weaviate Embeddings
const data = [
{ title: "The Matrix", description: "A hacker discovers reality is a simulation and joins a rebellion against the machines.", year: 1999 },
{ title: "Inception", description: "A thief who steals secrets through dream-sharing technology is given one final job, planting an idea.", year: 2010 },
{ title: "Interstellar", description: "Explorers travel through a wormhole in space to ensure humanity's survival.", year: 2014 },
{ title: "The Godfather", description: "The aging patriarch of a crime dynasty transfers control to his reluctant son.", year: 1972 },
{ title: "Spirited Away", description: "A young girl wanders into a world of spirits and must work in a bathhouse to free her parents.", year: 2001 },
{ title: "Toy Story", description: "A cowboy doll feels threatened when a new spaceman action figure becomes the favorite toy.", year: 1995 },
{ title: "Jaws", description: "A giant great white shark terrorizes a small beach community.", year: 1975 },
{ title: "La La Land", description: "A jazz pianist and an aspiring actress fall in love while chasing their dreams in Los Angeles.", year: 2016 },
{ title: "Mad Max: Fury Road", description: "In a post-apocalyptic wasteland, a drifter and a rebel warrior flee a tyrant in an armored war rig.", year: 2015 },
{ title: "Finding Nemo", description: "A timid clownfish crosses the ocean to rescue his son, who was captured by a diver.", year: 2003 },
];

// highlight-start
const result = await movies.data.insertMany(data); // Vectorized server-side by Weaviate Embeddings
// highlight-end

if (result.hasErrors) {
console.log(`Import errors: ${JSON.stringify(result.errors)}`);
} else {
const aggregate = await movies.aggregate.overAll();
console.log(`Imported ${aggregate.totalCount} movies`);
}

// Step 4: Semantic search
// The vector index updates in the background after import, so retry briefly
let response;
for (let attempt = 0; attempt < 15; attempt++) {
// highlight-start
response = await movies.query.nearText("a science fiction adventure in space", {
limit: 3,
returnMetadata: ["distance"],
});
// highlight-end
if (response.objects.length > 0) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}

for (const obj of response.objects) {
console.log(`${obj.properties.title} (${obj.properties.year}) — distance ${obj.metadata?.distance?.toFixed(3)}`);
}

await client.close(); // Free up resources
// END EndToEndExample
Loading
Loading