feat(bulk-load): assistant tools to use and create saved bulk load profiles #38

Merged
KofTwentyTwo merged 2 commits from feature/bulk-load-tools into develop 2026-10-14 12:05:52 +00:00
KofTwentyTwo commented 2026-10-14 02:47:25 +00:00 (Migrated from github.com)

On 2026-10-13 the owner asked that the assistant be able to use and create saved bulk load profiles. This PR adds the tools from spec QRun-IO/qqq#1171 section 3.4. Every call goes through the /qqq/v1 UI API as the user (tools/bulkload).

Tools

Tool UI API Kind
bulk_profiles_list(tableName) / bulk_profile_describe(id) POST /table/savedBulkLoadProfile/query, GET /table/savedBulkLoadProfile/{id} (record locks apply) read
bulk_file_attach(uploadRef, tableName) GET /bulkLoad/uploads/{ref}/profile read
bulk_mapping_suggest(uploadRef, tableName) POST /bulkLoad/{t}/suggestMapping. The model then refines the result from the headers and the permitted samples. read
bulk_load_dry_run(uploadRef, tableName, profile, mode) POST /bulkLoad/{t}/validate. Nothing is stored. read
bulk_profile_save(tableName, label, profile, shares?) Pending action, then on confirm: storeSavedBulkLoadProfile plus insertSharedRecord for each share write
bulk_load_run(tableName, uploadRef, profileId) Pending action, then on confirm: BulkLoadRunner, whose default is POST /processes/{t}.bulkInsert/jobs write

Rules enforced by the server or the engine, not the model

  • PHI: the engine sets X-QQQ-AI-Route: hipaa|standard from the turn's provider (active.row().getHipaaEligible()). Core decides which samples and values to return. Tool-call logs drop sampleValues, example bullets and valueMappings.
  • Writes: writes use the existing pending-action path and run only on the user's Confirm. Both proposing and confirming require DataAccess READ_WRITE. Sharing is only as far as core lets the user share (owner check).
  • Attachments: files reach the model only as pageContext.attachments {uploadRef, fileName}. That is at most 5 entries of scalars, so no bytes.
  • Durable jobs seam: BulkLoadRunner.start(request) -> RunHandle{status (jobs_job status names), jobId, processUUID, summary}. Launches from the assistant are created AWAITING_CONFIRMATION by core.

Stand-ins

  • STAND-IN(core=#1179, removeBy=RC.8): the bulkLoad profile, suggestMapping and validate operations (QRun-IO/qqq#1179) are not in RC.5. Until they ship, the tools tell the user the server does not offer them yet. This uses gateway.isInvocable and makes no call.
  • STAND-IN(core=#1171, removeBy=RC.8): the jobs launch, from the durable-jobs kernel PR.
  • The upload itself is core's POST /qqq/v1/uploads, built by the jobs-kernel agent. The Next panel's attach button follows once that lands.

Shared files touched (additive; the #19/#21 rebase must keep these)

  • UiApiCall.withFormParam / withHeader, mirroring core's UiApiRequest.withFormParam.
  • LoopbackUiApiGateway multipart bodies, plus forwarding of non-reserved headers.
  • UiApiToolbox.Outcome.of.
  • One branch each in ContextBuilder.tools, TurnEngine dispatch and AssistantApi.confirmAction.
  • sanitizePageContext attachments.
  • ToolLabels.
  • The AssistantQBitConfig.bulkLoadRunner seam.

Tests

BulkLoadToolsTest (6 tests):

  • Definitions are offered, and the write tools only when something is writable.
  • Read tools call the right operations with the user's session.
  • The route header always comes from the provider, even when the model's input contains X-QQQ-AI-Route.
  • 404 maps to NOT_FOUND.
  • DataAccess denial makes no call.
  • An older server (operations not invocable) makes no call and returns a clear message.
  • The jobs runner says when the launch is missing.
  • forLog strips samples and values.
  • Golden eval (scripted model standing in for the LLM, run against a fake UI API that answers like core's dry run):
    1. A CSV with messy headers is attached.
    2. Attach, then suggest.
    3. The dry run reports 2 rows missing required Status.
    4. The model reads the error and adds defaultValue: DRAFT.
    5. The FULL dry run reports 0 errors.
    6. The model proposes a save shared with group kof22-admins. Nothing is saved until the user confirms; after Confirm, storeSavedBulkLoadProfile and insertSharedRecord run with the right values.
    7. In a second turn, the model proposes the run. After Confirm, the runner receives profile 7 and up_1.
    8. No sample sentinel appears in any tool-call log.

The full ./mvnw verify (all modules, checkstyle, JaCoCo gates, ArchitectureTest, NamingTest, golden and red-team evals) is green.

🤖 Generated with Claude Code

On 2026-10-13 the owner asked that the assistant be able to use and create saved bulk load profiles. This PR adds the tools from spec QRun-IO/qqq#1171 section 3.4. Every call goes through the `/qqq/v1` UI API as the user (`tools/bulkload`). ## Tools | Tool | UI API | Kind | |---|---|---| | `bulk_profiles_list(tableName)` / `bulk_profile_describe(id)` | `POST /table/savedBulkLoadProfile/query`, `GET /table/savedBulkLoadProfile/{id}` (record locks apply) | read | | `bulk_file_attach(uploadRef, tableName)` | `GET /bulkLoad/uploads/{ref}/profile` | read | | `bulk_mapping_suggest(uploadRef, tableName)` | `POST /bulkLoad/{t}/suggestMapping`. The model then refines the result from the headers and the permitted samples. | read | | `bulk_load_dry_run(uploadRef, tableName, profile, mode)` | `POST /bulkLoad/{t}/validate`. Nothing is stored. | read | | `bulk_profile_save(tableName, label, profile, shares?)` | Pending action, then on confirm: `storeSavedBulkLoadProfile` plus `insertSharedRecord` for each share | write | | `bulk_load_run(tableName, uploadRef, profileId)` | Pending action, then on confirm: `BulkLoadRunner`, whose default is `POST /processes/{t}.bulkInsert/jobs` | write | ## Rules enforced by the server or the engine, not the model - **PHI:** the engine sets `X-QQQ-AI-Route: hipaa|standard` from the turn's provider (`active.row().getHipaaEligible()`). Core decides which samples and values to return. Tool-call logs drop `sampleValues`, example bullets and `valueMappings`. - **Writes:** writes use the existing pending-action path and run only on the user's Confirm. Both proposing and confirming require DataAccess READ_WRITE. Sharing is only as far as core lets the user share (owner check). - **Attachments:** files reach the model only as `pageContext.attachments` `{uploadRef, fileName}`. That is at most 5 entries of scalars, so no bytes. - **Durable jobs seam:** `BulkLoadRunner.start(request) -> RunHandle{status (jobs_job status names), jobId, processUUID, summary}`. Launches from the assistant are created `AWAITING_CONFIRMATION` by core. ## Stand-ins - `STAND-IN(core=#1179, removeBy=RC.8)`: the bulkLoad profile, suggestMapping and validate operations (QRun-IO/qqq#1179) are not in RC.5. Until they ship, the tools tell the user the server does not offer them yet. This uses `gateway.isInvocable` and makes no call. - `STAND-IN(core=#1171, removeBy=RC.8)`: the jobs launch, from the durable-jobs kernel PR. - The upload itself is core's `POST /qqq/v1/uploads`, built by the jobs-kernel agent. The Next panel's attach button follows once that lands. ## Shared files touched (additive; the #19/#21 rebase must keep these) - `UiApiCall.withFormParam` / `withHeader`, mirroring core's `UiApiRequest.withFormParam`. - `LoopbackUiApiGateway` multipart bodies, plus forwarding of non-reserved headers. - `UiApiToolbox.Outcome.of`. - One branch each in `ContextBuilder.tools`, `TurnEngine` dispatch and `AssistantApi.confirmAction`. - `sanitizePageContext` attachments. - `ToolLabels`. - The `AssistantQBitConfig.bulkLoadRunner` seam. ## Tests `BulkLoadToolsTest` (6 tests): - Definitions are offered, and the write tools only when something is writable. - Read tools call the right operations with the user's session. - The route header always comes from the provider, even when the model's input contains `X-QQQ-AI-Route`. - 404 maps to NOT_FOUND. - DataAccess denial makes no call. - An older server (operations not invocable) makes no call and returns a clear message. - The jobs runner says when the launch is missing. - `forLog` strips samples and values. - **Golden eval** (scripted model standing in for the LLM, run against a fake UI API that answers like core's dry run): 1. A CSV with messy headers is attached. 2. Attach, then suggest. 3. The dry run reports 2 rows missing required Status. 4. The model reads the error and adds `defaultValue: DRAFT`. 5. The FULL dry run reports 0 errors. 6. The model proposes a save shared with group `kof22-admins`. Nothing is saved until the user confirms; after Confirm, `storeSavedBulkLoadProfile` and `insertSharedRecord` run with the right values. 7. In a second turn, the model proposes the run. After Confirm, the runner receives profile 7 and `up_1`. 8. No sample sentinel appears in any tool-call log. The full `./mvnw verify` (all modules, checkstyle, JaCoCo gates, ArchitectureTest, NamingTest, golden and red-team evals) is green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.