prompts

Review of the initial version (from October 2025)

As you can see in this directory I want to have a Raku package that connects to H2O Java library/ platform. 
Examine the code and make recommendations. 
My biggest question is should I have a Raku proxy class for H2O's data frames, what would be its attributes and methods. 
If you need documentation of H2O says, so. 
The GitHub repository https://github.com/h2oai/h2o-3 has relevant code.

Answer:

I’ll inspect the package structure and the connector’s Java interop surface first. Then I’ll compare it with H2O’s frame model and give a concrete proxy API recommendation; I’ll call out any points that genuinely require upstream documentation or source verification.

The current connector is a REST client/process manager, not direct JVM interop. That distinction matters: the useful Raku object should proxy H2O REST resources (frame keys and endpoints), while preserving H2O’s server-side/lazy semantics instead of attempting to mirror Java Frame/Vec objects locally. I’m checking the examples and package contract now, including a few connector correctness issues that affect such a proxy.

A concrete constructor bug is already visible: H2O::Client.new("http://example.test:1234") silently resets the URL to 127.0.0.1:54321, and the embedded connector is never constructed/configured. I’m also validating H2O’s actual REST schemas (FrameV3, FramesV3) so the proxy recommendation follows stable wire-level fields rather than the much larger Python H2OFrame convenience API.

Guardian Review

Yes—add a Raku H2O::Client::Frame proxy. It should represent a remote H2O frame key, not copy Java’s Frame object or eagerly download data.

The package is currently best described as an H2O-3 REST client rather than direct Java-library interop. That is the right architectural direction.

Recommended frame proxy

class H2O::Client::Frame {
    has H2O::Client:D $.client is required;
    has Str:D        $.id     is required;

    has %!metadata;
    has Bool $!deleted = False;

    method refresh(:$light = True --> H2O::Client::Frame) { ... }
    method exists(--> Bool)                               { ... }

    method nrows(--> UInt)   { ... }
    method ncols(--> UInt)   { ... }
    method shape(--> List)   { ... }
    method names(--> Array)  { ... }
    method types(--> Map)    { ... }
    method checksum()        { ... }
    method byte-size()       { ... }

    method column(Str:D $name --> H2O::Client::Column) { ... }

    method preview(
        UInt :$rows = 10,
        UInt :$offset = 0,
             :@columns
        --> Array
    ) { ... }

    method head(UInt $rows = 10 --> Array) { ... }
    method tail(UInt $rows = 10 --> Array) { ... }
    method summary(:$column)               { ... }

    method download(IO::Path:D $path, *%options) { ... }
    method export(Str:D $server-path, *%options --> H2O::Client::Job) { ... }
    method delete() { ... }
    method raw()    { ... }
}

Only client and

id should be authoritative attributes. Rows, columns, names, types, checksum, and statistics should be cached metadata refreshed from /3/Frames/{id}. They should not be public rw attributes because the server owns that state.

A companion Column proxy is worthwhile:

class H2O::Client::Column {
    has H2O::Client::Frame:D $.frame;
    has Str:D $.name;

    method type()          { ... }
    method domain()        { ... }
    method summary()       { ... }
    method missing-count() { ... }
    method min()           { ... }
    method max()           { ... }
    method mean()          { ... }
    method sigma()         { ... }
    method percentiles()   { ... }
}

These properties correspond closely to H2O’s upstream FrameV3.ColV3.

I would allow $frame<Age> to return a column proxy eventually, but avoid positional indexing and overloaded arithmetic initially: hidden network operations and ambiguous row/column semantics become surprising.

A job proxy is equally important

Parsing, model building, prediction, and export are asynchronous in H2O. The current example starts operations and immediately queries frames/models.

Add H2O::Client::Job with:

  • id, destination-id, destination-type

  • status, progress, error

  • refresh, wait, cancel

  • result, returning a Frame or Model proxy when appropriate

Then importing can naturally become:

my $frame = $h2o.upload(@records, :id<titanic.hex>).wait.result;

Important current problems

  1. H2O::Client.new discards the supplied base URL. I verified that:

H2O::Client.new("http://example.test:1234").base-url

returns http://127.0.0.1:54321.

  1. $!conn is never initialized in H2O::Client, so delegated lifecycle methods and init are unreliable.

  2. Connector.!request reads $res<body>. This HTTP::Tiny implementation returns response bytes as $res<content>, so successful JSON requests return Nil.

  3. There are two incompatible HTTP layers. Connector sends JSON, while H2O::Client sends form data. Consolidate all HTTP behavior into one transport owned by H2O::Client; make Connector only responsible for launching/stopping a local H2O process.

  4. data-import(@data) writes a local CSV and calls /3/ImportFiles. That works only when the H2O server shares the same filesystem. Remote clients need /3/PostFile multipart upload. The temporary file is also not removed.

  5. Query parameters and frame/model IDs are not URL-encoded.

  6. HTTP failures should throw a typed exception containing status, URL, and H2O’s error body instead of returning Nil or an undecoded string.

  7. shutdown currently shuts down an externally managed cluster too. Separate stop-local-process from an explicit shutdown-cluster.

  8. HTTP::Tiny is used directly but missing from META6.json; several declared LLM/Text dependencies appear unused. There are no tests.

Suggested implementation order

  1. Unify and test the HTTP transport.

  2. Fix construction and connector lifecycle.

  3. Add Job.

  4. Add the minimal Frame and Column proxies.

  5. Make frames() return proxies, with :$raw for the current hashes.

  6. Make upload/parse/prediction return jobs or proxies.

  7. Add Rapids-backed transformations later.

Do not try to reproduce all 5,000 lines of Python H2OFrame. Start with identity, metadata, preview, summary, download/export, and deletion. For transformations, build a smaller explicit Rapids expression layer once the basic resource model is stable.

I did not need additional documentation for this design: the upstream FrameV3, FramesV3, FramesHandler, and Python H2OFrame sources were sufficient. Before implementation, I would choose a supported H2O baseline—probably the 3.46.x version already used by the example—and capture its /3/Metadata/endpoints and schema output for compatibility tests.

Implementation (core)

Ok. Do the suggested implementation steps. Add corresponding tests in the directories "./t" and "./xt". 
In "./t" put "easy" or sanity tests; in "./xt" tests that involve actual Java H2O execution.

Implementation extension (data frames transformations, Rapids)

I started the H2O cluster -- can be found at: http://localhost:54321 .
Proceed with implementing data frame(s)  identity, metadata, preview, summary, download/export, and deletion. 
For transformations, build a smaller explicit Rapids expression layer once the basic resource model is stable.

After I got the answer below.

Ok. Implement the lazy conversion.

Answer

I have the Rapids layer scaffolded and am resolving its Raku type signature before live execution. I’ll preserve your newly added credits while completing the proxy and integration coverage.

The Rapids API now compiles and its fast tests confirm AST construction, safe identifiers, expression-client ownership, and explicit tmp= materialization. I’m running the attached integration suite next; it creates only uniquely prefixed frames and temporary CSVs, then deletes them in cleanup.

Guardian Review

Guardian Review

The first attached run validated all basic frame endpoints and even completed one Rapids transformation. A second run exposed H2O’s Rapids session requirement: submitting without a /4/sessions key can leave server-side vector references behind. I’m adding lazy session creation and explicit client closure, matching H2O’s own Python client protocol.

Guardian Review

Implemented and live-validated the frame resource API against your running H2O cluster.

  • Frame now has stable cluster/key identity, lazy metadata, preview/head/tail, summaries, download, export jobs, deletion, and explicit Rapids entry points: Frame.rakumod

  • Added a small, safe, lazy Rapids expression layer: Expr.rakumod

  • Rapids now creates and uses an H2O session lazily, preventing server-side reference leaks: Client.rakumod

  • Documented the transformation API in README.md.

Example:

my $adults = $frame
    .where($frame<age>.expression.greater-than(18))
    .select(<age income>)
    .materialize('adults.hex');

All fast tests pass. The attached-cluster integration test also passed against http://localhost:54321, covering metadata, preview, summary, download, export, Rapids filtering/selection, and cleanup: xt/02-frame-and-rapids-live.rakutest.

Your resources/ directory was left untouched.

Implementation of AT-POS for data frames

For `H2O::Client::Frame` implement the method `AT-POS` that can take as argument integer index, list of indexes, range, `*`, or `Whatever`. 
It can be based on the method `preview`.

Transform column values to factors/enums

I want to have column conversions. 
For example, a column of integers to be converted to factors. 
Where it is best to do that? In the frame class or in the column class?

After I got the answer below.

Ok. Implement the lazy conversion.

Answer

Put column conversions on H2O::Client::Column. A conversion such as integer → factor is intrinsically a single-column transformation, so an API like this keeps the intent clear:

my $factor-column = $frame<age>.as-factor;

The method can return a new Frame (H2O transformations normally materialize a new server-side frame) or a Rapids expression that the caller explicitly materializes. I’d favor a lazy expression initially:

my $converted = $frame<age>.as-factor.materialize;

Frame should own frame-wide operations—selection, filtering, joins, adding/removing columns, or bulk conversion mappings such as convert-columns(%(age => 'factor', score => 'real')).

That gives a clean division:

  • Column: as-factor, as-int, as-real, as-string, possibly replace-missing.

  • Frame: operations involving multiple columns or the frame schema.

A useful detail: conversion should not mutate the source frame implicitly. Return a derived frame/expression, with an optional explicit replacement operation later if desired.

Grouping

In order to implement data grouping over the values of one or several columns -- 
should there be a separate `GroupBy` class or just a method in `H2O::Client::Rapids::Expr` would be sufficient? 
If H2O has a separate class of grouped data frames, I assume the client also should have corresponding proxy. 

Answer

Use a separate H2O::Client::Rapids::GroupBy builder, returned by an Expr.group-by method. H2O’s grouped-frame object is best treated as a client-side, lazy query plan—not a persistent server resource like Frame. Grouping only becomes a frame once an aggregation is requested and materialized.

my $grouped = $frame.expression.group-by(<region year>);

my $result = $grouped
    .count
    .mean('revenue')
    .materialize('revenue-by-region-year.hex');

GroupBy would retain:

  • the source Rapids::Expr

  • grouping column selectors

  • requested aggregations

Its aggregation methods return another lazy expression (or keep returning the builder until .expression/.materialize). This prevents Expr from accumulating stateful grouping configuration and leaves room for H2O’s GB Rapids operation and aggregation-specific validation.

So: separate class, yes—but a lightweight lazy builder/proxy, not a separately fetched H2O server object.

Implementation finished

I’ll add a lightweight lazy Rapids::GroupBy builder, expose it from expressions, and make its count and mean aggregations emit the H2O Rapids GB plan without creating a server-side object.

The builder now resolves column names to H2O’s required column indexes when the aggregation plan is compiled; integer selectors remain fully metadata-free. I’m running the complete test set and checking the final diff.

Implemented lazy grouping with GroupBy.rakumod.

my $result = $frame.expression
    .group-by(<region year>)
    .count
    .mean('revenue')
    .materialize('revenue-by-region-year.hex');

group-by returns a mutable lazy builder supporting count, mean, sum, min, and max; .expression, .evaluate, or .materialize compiles it into H2O’s GB Rapids expression. Named columns are resolved to H2O column indexes at compilation.

Also updated Expr.rakumod, package metadata, and grouping tests. All tests pass.

Import files (from URLs)

Implement the method import-file to produce a data frame job if the argument "job" is true. 
Call self.data-parse-setup and self.data-parse as it is done in upload-file. 
Do the appropriate refactoring between the two functions. 
Is there a better, more direct way using H2O.ai's REST API ?

Answer

[...]

H2O-3 has no one-call REST endpoint that both imports server-visible files and creates a parsed frame. The standard REST flow is exactly GET /3/ImportFilesPOST /3/ParseSetupPOST /3/Parse; skipping setup is only practical when every parse setting is already known.

Fixing extra row addition when uploading data with specified column names

When I use `H20::Client::upload` with column names I get data frames that have one additional row. 
See my change on row 31 of "./xt/02-frame-and-rapids-live.rakutest'. 
With that change 6 of the tests below it fail.

Answer

[...] I reproduced the six failures against the live cluster. The generated CSV is headed group,x, while the explicit names are x,group; that explains the downstream predicate failure, but I’m checking the exact H2O parse request semantics to address the extra header row too.

The cause is confirmed: H2O treats supplied column_names as an instruction to parse the first CSV line as data, regardless of check_header. I’ll make upload serialize records in the requested column order with those names as the CSV’s actual header, then let ParseSetup provide the matching parsed names instead of sending column_names again to Parse.

Fixed the extra-row issue in "Client.rakumod".

When explicit column-names are supplied, upload now:

  • Writes those names as the actual CSV header, in the requested order.

  • Orders associative record values to match those names.

  • Does not resend column_names to H2O’s Parse endpoint, since H2O otherwise treats the header as a data row.

This also fixes the mismatched x/group types that caused the Rapids filter failures.

Verified with:

  • prove6 -Ilib xt/02-frame-and-rapids-live.rakutest — pass, 16 tests

  • prove6 -Ilib t — pass, 57 tests

H2O::Client v0.0.2

Client for the open-source, distributed in-memory machine learning platform H2O (https://h2o.ai).

Authors

  • Anton Antonov

License

Apache-License-2.0

Dependencies

HTTP::Tiny:ver<0.2.5+>JSON::Fast:ver<0.17+>URI::EncodeData::Importers:ver<0.1.7+>

Test Dependencies

Provides

  • H2O::Client
  • H2O::Client::Column
  • H2O::Client::Connector
  • H2O::Client::Exception
  • H2O::Client::Frame
  • H2O::Client::Job
  • H2O::Client::Rapids::Expr
  • H2O::Client::Rapids::GroupBy
  • H2O::Client::Transport

The Camelia image is copyright 2009 by Larry Wall. "Raku" is a trademark of the Yet Another Society. All rights reserved.

Built with Podlite — the markup and publishing tools behind this site.