Scaffold: fork of Chambers architecture, renamed to Boardroom Map
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { sdk } from '../sdk'
|
||||
import { configFile } from '../file-models/config'
|
||||
|
||||
const { InputSpec, Value, List } = sdk
|
||||
|
||||
const inputSpec = InputSpec.of({
|
||||
reviewers: Value.list(
|
||||
List.obj(
|
||||
{
|
||||
name: 'Review Panel',
|
||||
description:
|
||||
'Who sits on the panel — one entry per review. Add as many as you like. ' +
|
||||
'Each reviewer is a model from your catalog plus a PERSONA: the lens it ' +
|
||||
'reads through, so the same document gets examined from different angles.',
|
||||
default: [],
|
||||
minLength: 1,
|
||||
maxLength: 32,
|
||||
},
|
||||
{
|
||||
uniqueBy: 'name',
|
||||
displayAs: '{{name}} ({{model}})',
|
||||
spec: InputSpec.of({
|
||||
name: Value.text({
|
||||
name: 'Name',
|
||||
description: 'Unique reviewer name. Becomes its container and report filename.',
|
||||
required: true,
|
||||
default: null,
|
||||
placeholder: 'risk-counsel',
|
||||
patterns: [
|
||||
{ regex: '^[A-Za-z0-9][A-Za-z0-9 _-]{0,40}$',
|
||||
description: 'Letters, numbers, spaces, dashes, underscores (max 41 chars).' },
|
||||
],
|
||||
}),
|
||||
model: Value.text({
|
||||
name: 'Model Alias',
|
||||
description: 'Which catalog model this reviewer uses (must match an alias from "Configure Models").',
|
||||
required: true,
|
||||
default: null,
|
||||
placeholder: 'reviewer-a',
|
||||
}),
|
||||
persona: Value.textarea({
|
||||
name: 'Persona / Lens',
|
||||
description:
|
||||
'How THIS reviewer should read the documents — its priorities and ' +
|
||||
'weighting. Injected into its system prompt. e.g. "You are skeptical ' +
|
||||
'legal counsel: weight liability, ambiguous obligations, and missing ' +
|
||||
'clauses above everything." Leave empty for a neutral reviewer.',
|
||||
required: false,
|
||||
default: null,
|
||||
minRows: 3,
|
||||
maxRows: 16,
|
||||
placeholder:
|
||||
'You are a financial-controls reviewer. Focus on numbers that do not ' +
|
||||
'reconcile, unstated assumptions behind projections, and anything that ' +
|
||||
'would concern an auditor. Organize findings by severity.',
|
||||
}),
|
||||
temperature: Value.number({
|
||||
name: 'Sampling Temperature (optional)',
|
||||
description:
|
||||
'Best-effort per-reviewer sampling temperature for extra diversity. ' +
|
||||
'Persona is the primary lever. Leave empty to use the model default.',
|
||||
required: false,
|
||||
default: null,
|
||||
integer: false,
|
||||
min: 0,
|
||||
max: 2,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
})
|
||||
|
||||
export const configureReviewers = sdk.Action.withInput(
|
||||
'configure-reviewers',
|
||||
|
||||
async ({ effects }) => ({
|
||||
name: 'Configure Reviewers',
|
||||
description: 'Define the review panel: which models and which personas, and how many reviews.',
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
inputSpec,
|
||||
|
||||
async ({ effects }) => {
|
||||
const cfg = await configFile.read().const(effects)
|
||||
if (!cfg) return {}
|
||||
return { reviewers: cfg.reviewers }
|
||||
},
|
||||
|
||||
async ({ effects, input }) => {
|
||||
await configFile.merge(effects, { reviewers: input.reviewers })
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: 'Panel Configured',
|
||||
message:
|
||||
'Saved a panel of ' + input.reviewers.length + ' reviewer(s). Each model ' +
|
||||
'alias must exist in "Configure Models". Set the rubric in "Configure ' +
|
||||
'Review", then drop documents and run a review.',
|
||||
result: {
|
||||
type: 'single',
|
||||
value: input.reviewers.map((r) => r.name).join(', '),
|
||||
copyable: false,
|
||||
qr: false,
|
||||
masked: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,132 @@
|
||||
import { sdk } from '../sdk'
|
||||
import { configFile } from '../file-models/config'
|
||||
|
||||
const { InputSpec, Value } = sdk
|
||||
|
||||
const inputSpec = InputSpec.of({
|
||||
reviewInstructions: Value.textarea({
|
||||
name: 'Review Rubric',
|
||||
description:
|
||||
'What every reviewer should look for and produce. Layered above each ' +
|
||||
'reviewer\'s persona. Be concrete about the structure you want back.',
|
||||
required: true,
|
||||
default: null,
|
||||
minRows: 5,
|
||||
maxRows: 20,
|
||||
placeholder:
|
||||
'Review the attached document(s). Produce: a short summary, key findings, ' +
|
||||
'risks/red flags, open questions, and recommendations. Cite the document and ' +
|
||||
'section for each point. Never invent facts not present in the documents.',
|
||||
}),
|
||||
networkMode: Value.select({
|
||||
name: 'Network Mode',
|
||||
description:
|
||||
'Air-gapped: reviewers reach ONLY the on-Spark model proxy — zero internet, ' +
|
||||
'documents never leave your hardware (models must be pre-pulled into the ' +
|
||||
'Spark HF cache, all on the head Spark). Local services: reviewers may also ' +
|
||||
'reach LAN services like SearXNG and the second Spark (this network has ' +
|
||||
'egress unless you firewall it).',
|
||||
default: 'airgapped',
|
||||
values: {
|
||||
airgapped: 'Air-gapped (no network, recommended)',
|
||||
local_services: 'Local services (SearXNG / 2nd Spark)',
|
||||
},
|
||||
}),
|
||||
searxngUrl: Value.text({
|
||||
name: 'SearXNG URL (local-services only)',
|
||||
description: 'JSON-search endpoint to give reviewers a web_search tool. Ignored in air-gapped mode. Empty = no web search.',
|
||||
required: false,
|
||||
default: null,
|
||||
placeholder: 'https://searxng.local',
|
||||
}),
|
||||
synthesisEnabled: Value.toggle({
|
||||
name: 'Synthesize a Consolidated Report',
|
||||
description:
|
||||
'After the panel finishes, run a local "lead reviewer" that reads all the ' +
|
||||
'individual reports and writes one consolidated report (themes, conflicts, ' +
|
||||
'consensus, recommendation). No frontier model — stays on the Sparks.',
|
||||
default: true,
|
||||
}),
|
||||
synthesisModel: Value.text({
|
||||
name: 'Lead Reviewer Model (optional)',
|
||||
description: 'Catalog alias of the model that writes the consolidated report. Empty = use the first model.',
|
||||
required: false,
|
||||
default: null,
|
||||
placeholder: 'reviewer-a',
|
||||
}),
|
||||
synthesisPersona: Value.textarea({
|
||||
name: 'Lead Reviewer Instructions (optional)',
|
||||
description: 'Override how the consolidated report is written. Empty = a sensible built-in default.',
|
||||
required: false,
|
||||
default: null,
|
||||
minRows: 3,
|
||||
maxRows: 14,
|
||||
}),
|
||||
wipeRemoteDocs: Value.toggle({
|
||||
name: 'Wipe Documents From Sparks After Review',
|
||||
description:
|
||||
'Delete the extracted document text from the Sparks when a job finishes. ' +
|
||||
'Reports are always kept on this StartOS box. Recommended for confidential material.',
|
||||
default: true,
|
||||
}),
|
||||
autoRunOnDrop: Value.toggle({
|
||||
name: 'Auto-run When Documents Are Dropped',
|
||||
description:
|
||||
'Start a review automatically (after a short debounce) whenever new files ' +
|
||||
'land in the inbox. Off by default so you trigger reviews explicitly.',
|
||||
default: false,
|
||||
}),
|
||||
})
|
||||
|
||||
export const configureReview = sdk.Action.withInput(
|
||||
'configure-review',
|
||||
|
||||
async ({ effects }) => ({
|
||||
name: 'Configure Review',
|
||||
description: 'Set the rubric, air-gap mode, synthesis, and document retention.',
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
inputSpec,
|
||||
|
||||
async ({ effects }) => {
|
||||
const cfg = await configFile.read().const(effects)
|
||||
if (!cfg) return {}
|
||||
return {
|
||||
reviewInstructions: cfg.reviewInstructions,
|
||||
networkMode: cfg.networkMode,
|
||||
searxngUrl: cfg.searxngUrl || undefined,
|
||||
synthesisEnabled: cfg.synthesisEnabled,
|
||||
synthesisModel: cfg.synthesisModel || undefined,
|
||||
synthesisPersona: cfg.synthesisPersona || undefined,
|
||||
wipeRemoteDocs: cfg.wipeRemoteDocs,
|
||||
autoRunOnDrop: cfg.autoRunOnDrop,
|
||||
}
|
||||
},
|
||||
|
||||
async ({ effects, input }) => {
|
||||
await configFile.merge(effects, {
|
||||
reviewInstructions: input.reviewInstructions,
|
||||
networkMode: input.networkMode,
|
||||
searxngUrl: input.searxngUrl ?? '',
|
||||
synthesisEnabled: input.synthesisEnabled,
|
||||
synthesisModel: input.synthesisModel ?? '',
|
||||
synthesisPersona: input.synthesisPersona ?? '',
|
||||
wipeRemoteDocs: input.wipeRemoteDocs,
|
||||
autoRunOnDrop: input.autoRunOnDrop,
|
||||
})
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: 'Review Settings Saved',
|
||||
message:
|
||||
input.networkMode === 'airgapped'
|
||||
? 'Saved. Reviewers will run air-gapped (no internet). Ensure all models are on the head Spark and pre-pulled into its HF cache.'
|
||||
: 'Saved. Reviewers run in local-services mode and may reach the network — make sure that is acceptable for these documents.',
|
||||
result: { type: 'single', value: input.networkMode, copyable: false, qr: false, masked: false },
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,159 @@
|
||||
import { sdk } from '../sdk'
|
||||
import { configFile } from '../file-models/config'
|
||||
|
||||
const { InputSpec, Value, List } = sdk
|
||||
|
||||
const inputSpec = InputSpec.of({
|
||||
models: Value.list(
|
||||
List.obj(
|
||||
{
|
||||
name: 'Model Catalog',
|
||||
description:
|
||||
'The local models this service can serve on your Sparks. Each reviewer ' +
|
||||
'references one of these by its alias. The job runner loads models in ' +
|
||||
'waves so you can run a panel across more models than fit in GPU memory ' +
|
||||
'at once.',
|
||||
default: [],
|
||||
minLength: 1,
|
||||
maxLength: 16,
|
||||
},
|
||||
{
|
||||
uniqueBy: 'alias',
|
||||
displayAs: '{{alias}} → {{hfModel}}',
|
||||
spec: InputSpec.of({
|
||||
alias: Value.text({
|
||||
name: 'Alias',
|
||||
description: 'Short name reviewers use to pick this model (e.g. "qwen-32b").',
|
||||
required: true,
|
||||
default: null,
|
||||
placeholder: 'reviewer-a',
|
||||
patterns: [
|
||||
{ regex: '^[a-z0-9][a-z0-9-]{0,30}$',
|
||||
description: 'Lowercase letters, numbers, dashes (max 31 chars).' },
|
||||
],
|
||||
}),
|
||||
hfModel: Value.text({
|
||||
name: 'Hugging Face Model ID',
|
||||
description: 'The model vLLM serves. Must be present in the Spark HF cache for air-gapped mode.',
|
||||
required: true,
|
||||
default: null,
|
||||
placeholder: 'Qwen/Qwen3-32B-FP8',
|
||||
}),
|
||||
spark: Value.select({
|
||||
name: 'Served On',
|
||||
description:
|
||||
'Which Spark serves this model. Air-gapped review mode requires the ' +
|
||||
'head (primary) Spark; the secondary is used only in local-services mode.',
|
||||
default: 'primary',
|
||||
values: { primary: 'Primary (head) Spark', secondary: 'Secondary Spark' },
|
||||
}),
|
||||
port: Value.number({
|
||||
name: 'vLLM Port',
|
||||
description: 'Host port the vLLM container for this model listens on. Unique per Spark.',
|
||||
required: true,
|
||||
default: 8001,
|
||||
integer: true,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
gpuMemoryUtilization: Value.text({
|
||||
name: 'GPU Memory Utilization',
|
||||
description: 'vLLM --gpu-memory-utilization (0–1). Lower it if you co-resident multiple models per Spark.',
|
||||
required: true,
|
||||
default: '0.85',
|
||||
}),
|
||||
maxModelLen: Value.number({
|
||||
name: 'Max Model Length',
|
||||
description: 'vLLM --max-model-len (context window). Documents are chunked to fit.',
|
||||
required: true,
|
||||
default: 32768,
|
||||
integer: true,
|
||||
min: 2048,
|
||||
}),
|
||||
toolCallParser: Value.text({
|
||||
name: 'Tool-Call Parser',
|
||||
description:
|
||||
'vLLM tool-call parser for the reviewer\'s read-file tool loop. Match the ' +
|
||||
'served model family (Qwen3 → "hermes"). Empty disables native tool-calling.',
|
||||
required: false,
|
||||
default: 'hermes',
|
||||
}),
|
||||
maxConcurrentModels: Value.number({
|
||||
name: 'Max Co-resident Models (head Spark)',
|
||||
description:
|
||||
'How many distinct models may load on the head Spark at once. The job runner ' +
|
||||
'loads models in waves so it never exceeds this. 1 is safest.',
|
||||
required: true,
|
||||
default: 1,
|
||||
integer: true,
|
||||
min: 1,
|
||||
max: 8,
|
||||
}),
|
||||
proxyPort: Value.number({
|
||||
name: 'Model Proxy Port',
|
||||
description: 'Port for the on-Spark LiteLLM router that exposes every model alias on one endpoint.',
|
||||
required: true,
|
||||
default: 4000,
|
||||
integer: true,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
}),
|
||||
})
|
||||
|
||||
export const configureModels = sdk.Action.withInput(
|
||||
'configure-models',
|
||||
|
||||
async ({ effects }) => ({
|
||||
name: 'Configure Models',
|
||||
description: 'Define the local model catalog served on your Sparks and the serving knobs.',
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
inputSpec,
|
||||
|
||||
async ({ effects }) => {
|
||||
const cfg = await configFile.read().const(effects)
|
||||
if (!cfg) return {}
|
||||
return {
|
||||
models: cfg.models,
|
||||
gpuMemoryUtilization: cfg.gpuMemoryUtilization,
|
||||
maxModelLen: cfg.maxModelLen,
|
||||
toolCallParser: cfg.toolCallParser,
|
||||
maxConcurrentModels: cfg.maxConcurrentModels,
|
||||
proxyPort: cfg.proxyPort,
|
||||
}
|
||||
},
|
||||
|
||||
async ({ effects, input }) => {
|
||||
await configFile.merge(effects, {
|
||||
models: input.models,
|
||||
gpuMemoryUtilization: input.gpuMemoryUtilization,
|
||||
maxModelLen: input.maxModelLen,
|
||||
toolCallParser: input.toolCallParser ?? '',
|
||||
maxConcurrentModels: input.maxConcurrentModels,
|
||||
proxyPort: input.proxyPort,
|
||||
})
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: 'Models Configured',
|
||||
message:
|
||||
'Saved ' + input.models.length + ' model(s). Make sure each is present in ' +
|
||||
'the Spark HF cache for air-gapped runs, then set "Configure Reviewers".',
|
||||
result: {
|
||||
type: 'single',
|
||||
value: input.models.map((m) => m.alias).join(', '),
|
||||
copyable: false,
|
||||
qr: false,
|
||||
masked: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,161 @@
|
||||
import { sdk } from '../sdk'
|
||||
import { configFile } from '../file-models/config'
|
||||
import { sshKeyFile, hfTokenFile } from '../file-models/secrets'
|
||||
|
||||
const { InputSpec, Value } = sdk
|
||||
|
||||
const inputSpec = InputSpec.of({
|
||||
primarySparkHost: Value.text({
|
||||
name: 'Primary Spark Host',
|
||||
description: 'Hostname or IP of the head DGX Spark (reachable over SSH). Serves models, hosts the model proxy, and runs the reviewer panel.',
|
||||
required: true,
|
||||
default: null,
|
||||
placeholder: 'spark-01.local',
|
||||
}),
|
||||
primarySparkUser: Value.text({
|
||||
name: 'SSH User',
|
||||
description: 'The login user on the Spark (DGX OS default is "nvidia").',
|
||||
required: true,
|
||||
default: 'nvidia',
|
||||
}),
|
||||
sshPort: Value.number({
|
||||
name: 'SSH Port',
|
||||
description: 'SSH port on the Spark.',
|
||||
required: true,
|
||||
default: 22,
|
||||
integer: true,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
}),
|
||||
sshPrivateKey: Value.textarea({
|
||||
name: 'SSH Private Key',
|
||||
description:
|
||||
'A private key (PEM/OpenSSH) whose public half is in the Spark user\'s ' +
|
||||
'~/.ssh/authorized_keys. Stored in this service\'s private volume and ' +
|
||||
'used only to reach your Sparks. Paste the FULL key including header/footer.',
|
||||
warning:
|
||||
'This is a credential. It is written to the service volume and never ' +
|
||||
'shown again. Use a dedicated key for this service.',
|
||||
required: true,
|
||||
default: null,
|
||||
minRows: 6,
|
||||
maxRows: 14,
|
||||
placeholder: '-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----',
|
||||
}),
|
||||
useBothSparks: Value.toggle({
|
||||
name: 'Use Both Sparks',
|
||||
description:
|
||||
'Allow models to be served on a second Spark (over ConnectX/200GbE) for ' +
|
||||
'extra capacity. NOTE: in air-gapped review mode all models must run on the ' +
|
||||
'head Spark; the second Spark is used only in local-services mode.',
|
||||
default: false,
|
||||
}),
|
||||
secondarySparkHost: Value.text({
|
||||
name: 'Secondary Spark Host',
|
||||
description: 'Hostname/IP of the second Spark. Required only if "Use Both Sparks" is on.',
|
||||
required: false,
|
||||
default: null,
|
||||
placeholder: 'spark-02.local',
|
||||
}),
|
||||
headInternalHost: Value.text({
|
||||
name: 'Head Internal Host',
|
||||
description:
|
||||
'Address the head Spark uses for its own preflight checks against the local ' +
|
||||
'model proxy. Single Spark: 127.0.0.1 is fine.',
|
||||
required: true,
|
||||
default: '127.0.0.1',
|
||||
}),
|
||||
remoteWorkDir: Value.text({
|
||||
name: 'Remote Work Directory',
|
||||
description: 'Absolute path on the head Spark for staged document text, the HF cache, and logs.',
|
||||
required: true,
|
||||
default: '/home/nvidia/boardroom-map',
|
||||
}),
|
||||
servingImage: Value.text({
|
||||
name: 'vLLM Image Tag',
|
||||
description: 'The vLLM serving image built on the Sparks (e.g. via spark-vllm-docker).',
|
||||
required: true,
|
||||
default: 'boardroom-vllm:latest',
|
||||
}),
|
||||
graderImage: Value.text({
|
||||
name: 'Reviewer Image Tag',
|
||||
description: 'The sandboxed reviewer image built on the head Spark from sandbox/build.sh.',
|
||||
required: true,
|
||||
default: 'boardroom-grader:latest',
|
||||
}),
|
||||
hfToken: Value.text({
|
||||
name: 'Hugging Face Token (optional)',
|
||||
description:
|
||||
'Only needed for gated/private models or to warm a model the first time. ' +
|
||||
'Leave empty to keep the existing token unchanged. Passed to the vLLM ' +
|
||||
'container at serve time.',
|
||||
required: false,
|
||||
default: null,
|
||||
masked: true,
|
||||
}),
|
||||
})
|
||||
|
||||
export const configureSparks = sdk.Action.withInput(
|
||||
'configure-sparks',
|
||||
|
||||
async ({ effects }) => ({
|
||||
name: 'Configure Sparks',
|
||||
description: 'Set the DGX Spark connection details, SSH credentials, and image tags.',
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
inputSpec,
|
||||
|
||||
// Prefill non-secret fields from existing config. Never prefill the key/token.
|
||||
async ({ effects }) => {
|
||||
const cfg = await configFile.read().const(effects)
|
||||
if (!cfg) return {}
|
||||
return {
|
||||
primarySparkHost: cfg.primarySparkHost || undefined,
|
||||
primarySparkUser: cfg.primarySparkUser,
|
||||
sshPort: cfg.sshPort,
|
||||
useBothSparks: cfg.useBothSparks,
|
||||
secondarySparkHost: cfg.secondarySparkHost ?? undefined,
|
||||
headInternalHost: cfg.headInternalHost,
|
||||
remoteWorkDir: cfg.remoteWorkDir,
|
||||
servingImage: cfg.servingImage,
|
||||
graderImage: cfg.graderImage,
|
||||
}
|
||||
},
|
||||
|
||||
async ({ effects, input }) => {
|
||||
// Persist the private key to its own file (600 enforced in-container).
|
||||
await sshKeyFile.write(effects, input.sshPrivateKey.trim() + '\n')
|
||||
|
||||
let hfTokenSet = (await configFile.read().const(effects))?.hfTokenSet ?? false
|
||||
if (input.hfToken && input.hfToken.trim()) {
|
||||
await hfTokenFile.write(effects, input.hfToken.trim())
|
||||
hfTokenSet = true
|
||||
}
|
||||
|
||||
await configFile.merge(effects, {
|
||||
primarySparkHost: input.primarySparkHost,
|
||||
primarySparkUser: input.primarySparkUser,
|
||||
sshPort: input.sshPort,
|
||||
useBothSparks: input.useBothSparks,
|
||||
secondarySparkHost: input.secondarySparkHost,
|
||||
headInternalHost: input.headInternalHost,
|
||||
remoteWorkDir: input.remoteWorkDir,
|
||||
servingImage: input.servingImage,
|
||||
graderImage: input.graderImage,
|
||||
hfTokenSet,
|
||||
})
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: 'Sparks Configured',
|
||||
message:
|
||||
'Saved. Use "Test Spark Connection" to verify SSH + GPU access, then set ' +
|
||||
'"Configure Models" and "Configure Reviewers".',
|
||||
result: { type: 'single', value: input.primarySparkHost, copyable: false, qr: false, masked: false },
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,59 @@
|
||||
import { startSdk } from '@start9labs/start-sdk'
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
/**
|
||||
* Trigger a review of whatever is currently in the inbox. The running job-runner
|
||||
* thread polls for /data/state/run_request and starts a job when it appears, so
|
||||
* this action just drops that request file (decoupled from the daemon — no need
|
||||
* to reach its HTTP port from the action's one-shot container).
|
||||
*/
|
||||
export const runReview = sdk.Action.withoutInput(
|
||||
'run-review',
|
||||
|
||||
async ({ effects }) => ({
|
||||
name: 'Run Review',
|
||||
description: 'Convene the panel now over the documents currently in the inbox.',
|
||||
warning: null,
|
||||
allowedStatuses: 'only-running',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
async ({ effects }) => {
|
||||
const mounts = sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
mountpoint: '/data',
|
||||
subpath: null,
|
||||
readonly: false,
|
||||
})
|
||||
|
||||
let output: string
|
||||
try {
|
||||
const { stdout } = await startSdk.runCommand<typeof sdk.manifest>(
|
||||
effects,
|
||||
{ imageId: 'main' },
|
||||
[
|
||||
'sh',
|
||||
'-c',
|
||||
'mkdir -p /data/state && date +%s > /data/state/run_request && ' +
|
||||
'n=$(ls -1 /data/inbox 2>/dev/null | wc -l | tr -d " "); ' +
|
||||
'echo "Review requested. $n file(s) in the inbox."',
|
||||
],
|
||||
{ mounts, env: { BM_DATA_DIR: '/data' } },
|
||||
'run-review',
|
||||
)
|
||||
output = (stdout?.toString() || '').trim() || 'Review requested.'
|
||||
} catch (e: any) {
|
||||
output = 'Could not request a review: ' + (e?.message || String(e))
|
||||
}
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: 'Review Requested',
|
||||
message:
|
||||
output +
|
||||
' Watch the Web UI for progress; reports appear there and via "View Latest Report".',
|
||||
result: { type: 'single', value: output, copyable: false, qr: false, masked: false },
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
import { sdk } from '../sdk'
|
||||
import { configureSparks } from './configure-sparks'
|
||||
import { configureModels } from './configure-models'
|
||||
import { configureReviewers } from './configure-reviewers'
|
||||
import { configureReview } from './configure-review'
|
||||
import { runReview } from './run-review'
|
||||
import { testConnection } from './test-connection'
|
||||
import { latestReport } from './latest-report'
|
||||
|
||||
export const actions = sdk.Actions.of()
|
||||
.addAction(configureSparks)
|
||||
.addAction(configureModels)
|
||||
.addAction(configureReviewers)
|
||||
.addAction(configureReview)
|
||||
.addAction(runReview)
|
||||
.addAction(testConnection)
|
||||
.addAction(latestReport)
|
||||
@@ -0,0 +1,51 @@
|
||||
import { startSdk } from '@start9labs/start-sdk'
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
/**
|
||||
* Returns the latest report as a copyable result, so you can read it straight
|
||||
* from the StartOS service page without opening the Web UI. The job runner saves
|
||||
* the most recent report (consolidated if synthesis is on, else the panel
|
||||
* digest) to /data/reports/latest.md on the StartOS host — no Spark round-trip.
|
||||
*/
|
||||
export const latestReport = sdk.Action.withoutInput(
|
||||
'latest-report',
|
||||
|
||||
async ({ effects }) => ({
|
||||
name: 'View Latest Report',
|
||||
description: 'Show the most recent review report produced by the panel.',
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
async ({ effects }) => {
|
||||
const mounts = sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
mountpoint: '/data',
|
||||
subpath: null,
|
||||
readonly: true,
|
||||
})
|
||||
|
||||
let report: string
|
||||
try {
|
||||
const { stdout } = await startSdk.runCommand<typeof sdk.manifest>(
|
||||
effects,
|
||||
{ imageId: 'main' },
|
||||
['sh', '-c', 'cat /data/reports/latest.md 2>/dev/null || echo "(no report yet — drop documents in the inbox and run a review)"'],
|
||||
{ mounts, env: { BM_DATA_DIR: '/data' } },
|
||||
'latest-report',
|
||||
)
|
||||
report = (stdout?.toString() || '').trim() || '(no report yet)'
|
||||
} catch (e: any) {
|
||||
report = 'Could not read report: ' + (e?.message || String(e))
|
||||
}
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: 'Latest Boardroom Map Report',
|
||||
message: 'The panel\'s most recent review.',
|
||||
result: { type: 'single', value: report, copyable: true, qr: false, masked: false },
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
import { startSdk } from '@start9labs/start-sdk'
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
/**
|
||||
* Runs a one-shot in the orchestrator image that SSHes into the configured
|
||||
* Spark(s) and reports `nvidia-smi` plus whether the vLLM + reviewer images are
|
||||
* built. Reuses orchestrator/spark_client.py so SSH logic lives in one place.
|
||||
*/
|
||||
export const testConnection = sdk.Action.withoutInput(
|
||||
'test-connection',
|
||||
|
||||
async ({ effects }) => ({
|
||||
name: 'Test Spark Connection',
|
||||
description: 'SSH into the configured Spark(s) and verify GPU + serving/reviewer image access.',
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
async ({ effects }) => {
|
||||
const mounts = sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
mountpoint: '/data',
|
||||
subpath: null,
|
||||
readonly: true,
|
||||
})
|
||||
|
||||
let output: string
|
||||
try {
|
||||
const { stdout, stderr } = await startSdk.runCommand<typeof sdk.manifest>(
|
||||
effects,
|
||||
{ imageId: 'main' },
|
||||
['python3', '/app/spark_client.py', 'test'],
|
||||
{ mounts, env: { BM_DATA_DIR: '/data' } },
|
||||
'spark-test',
|
||||
)
|
||||
output =
|
||||
(stdout?.toString() || '').trim() +
|
||||
(stderr?.toString().trim() ? '\n\n[stderr]\n' + stderr.toString().trim() : '')
|
||||
} catch (e: any) {
|
||||
output =
|
||||
'Connection test failed.\n\n' +
|
||||
(e?.stdout?.toString() || '') +
|
||||
(e?.stderr?.toString() || e?.message || String(e))
|
||||
}
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: 'Spark Connection Test',
|
||||
message: 'Result of probing your Spark(s) over SSH.',
|
||||
result: { type: 'single', value: output || '(no output)', copyable: true, qr: false, masked: false },
|
||||
}
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user