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
33 changes: 33 additions & 0 deletions packages/components/credentials/JungleGridApi.credential.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { INodeParams, INodeCredential } from '../src/Interface'

class JungleGridApiCredential implements INodeCredential {
label: string
name: string
version: number
description: string
inputs: INodeParams[]

constructor() {
this.label = 'Jungle Grid API'
this.name = 'jungleGridApi'
this.version = 1.0
this.description =
'Use a Jungle Grid API key to estimate, submit, monitor, cancel, and retrieve artifacts for long-running workloads.'
this.inputs = [
{
label: 'Jungle Grid API Key',
name: 'apiKey',
type: 'password'
},
{
label: 'Jungle Grid API Base URL',
name: 'baseUrl',
type: 'url',
default: 'https://api.junglegrid.dev',
description: 'Override only for development or self-hosted Jungle Grid orchestrators.'
}
]
}
}

module.exports = { credClass: JungleGridApiCredential }
77 changes: 77 additions & 0 deletions packages/components/nodes/tools/JungleGrid/JungleGrid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam } from '../../../src/utils'
import { createJungleGridTools, DEFAULT_JUNGLE_GRID_BASE_URL, JungleGridAction, JungleGridClient } from './core'
import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'

const ALL_ACTIONS: { label: string; name: JungleGridAction }[] = [
{ label: 'Estimate Job', name: 'estimateJob' },
{ label: 'Submit Job', name: 'submitJob' },
{ label: 'List Jobs', name: 'listJobs' },
{ label: 'Get Job', name: 'getJob' },
{ label: 'Get Job Runtime', name: 'getJobRuntime' },
{ label: 'Cancel Job', name: 'cancelJob' },
{ label: 'Get Job Logs', name: 'getJobLogs' },
{ label: 'List Job Artifacts', name: 'listJobArtifacts' },
{ label: 'Get Artifact Download URL', name: 'getArtifactDownloadUrl' }
]

class JungleGrid_Tools implements INode {
label: string
name: string
version: number
type: string
icon: string
category: string
description: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
documentation?: string

constructor() {
this.label = 'Jungle Grid'
this.name = 'jungleGrid'
this.version = 1.0
this.type = 'JungleGrid'
this.icon = 'junglegrid.svg'
this.category = 'Tools'
this.description = 'Estimate, submit, monitor, cancel, and retrieve artifacts for asynchronous Jungle Grid workloads'
this.documentation = 'https://junglegrid.dev/docs/mcp'
this.baseClasses = [this.type, 'Tool']
this.credential = {
label: 'Jungle Grid Credential',
name: 'credential',
type: 'credential',
credentialNames: ['jungleGridApi']
}
this.inputs = [
{
label: 'Actions',
name: 'actions',
type: 'multiOptions',
options: ALL_ACTIONS,
default: [
'estimateJob',
'submitJob',
'getJob',
'getJobRuntime',
'getJobLogs',
'listJobArtifacts',
'getArtifactDownloadUrl'
],
description: 'Choose which Jungle Grid tools to expose to the agent.'
}
]
}

async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const apiKey = getCredentialParam('apiKey', credentialData, nodeData) as string
const baseUrl = (getCredentialParam('baseUrl', credentialData, nodeData) as string) || DEFAULT_JUNGLE_GRID_BASE_URL
const actions = convertMultiOptionsToStringArray(nodeData.inputs?.actions) as JungleGridAction[]

const client = new JungleGridClient({ apiKey, baseUrl })
return createJungleGridTools(client, actions)
}
}

module.exports = { nodeClass: JungleGrid_Tools }
75 changes: 75 additions & 0 deletions packages/components/nodes/tools/JungleGrid/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Jungle Grid

The Jungle Grid tool node lets Flowise agents estimate, submit, monitor, cancel, and retrieve artifacts for asynchronous Jungle Grid workloads.

Jungle Grid acts as the durable execution layer for long-running AI workloads while Flowise remains the orchestration and visual agent-building layer.

## Credentials

Create a `Jungle Grid API` credential with:

- `Jungle Grid API Key`: a Jungle Grid API key.
- `Jungle Grid API Base URL`: defaults to `https://api.junglegrid.dev`. Override only for development or self-hosted orchestrators.

Do not place Jungle Grid API keys in prompts, command arguments, environment variables, source code, exported flows, or logs.

## Actions

- `Estimate Job`: calls `POST /v1/jobs/estimate` before starting work. Use this when cost, capacity, routing, or GPU tier matters.
- `Submit Job`: calls `POST /v1/jobs` and returns immediately with a `job_id`. A returned `job_id` does not mean the job is complete.
- `List Jobs`: calls `GET /v1/jobs` with verified `limit` and `status` filters.
- `Get Job`: calls `GET /v1/jobs/{job_id}` to retrieve current status and details.
- `Get Job Runtime`: calls `GET /v1/jobs/{job_id}/runtime` for stdout/stderr tails, exit information, diagnostics, and runtime availability.
- `Cancel Job`: calls `POST /v1/jobs/{job_id}/cancel` with an optional reason.
- `Get Job Logs`: uses the verified runtime endpoint to return available stdout/stderr and exit information.
- `List Job Artifacts`: calls `GET /v1/jobs/{job_id}/artifacts`.
- `Get Artifact Download URL`: calls `POST /v1/jobs/{job_id}/artifacts/{artifact_id}/download`.

Live log streaming is intentionally not exposed as a Flowise tool action. The official stream route is long-lived Server-Sent Events, while Flowise agent tools execute synchronously. Polling `Get Job`, `Get Job Runtime`, and `Get Job Logs` is the production-safe Flowise path.

## Usage Pattern

```text
Estimate Job
-> Submit Job
-> store job_id
-> Get Job / Get Job Runtime / Get Job Logs
-> wait for terminal status
-> List Job Artifacts
-> Get Artifact Download URL
```

`Submit Job` is asynchronous. It returns a `job_id` immediately, but that does not mean the workload has completed. Poll `Get Job`, `Get Job Runtime`, or `Get Job Logs` until Jungle Grid reports a terminal status. Retrieve artifacts after successful completion unless the API response explicitly shows partial outputs are available.

## Example Workloads

Minimal inference smoke-test shape:

```json
{
"name": "flowise-jungle-grid-smoke-test",
"image": "python:3.11",
"workload_type": "inference",
"model_size_gb": 1,
"command": "python",
"args": ["-c", "print(42)"]
}
```

Artifact-producing shape:

```json
{
"name": "flowise-jungle-grid-artifact-test",
"image": "python:3.11",
"workload_type": "batch",
"model_size_gb": 1,
"command": "python",
"args": [
"-c",
"import json, os; os.makedirs('/workspace/artifacts', exist_ok=True); json.dump({'status':'ok'}, open('/workspace/artifacts/output.json','w'))"
]
}
```

Common use cases include running inference workloads, batch jobs, evaluation workloads, artifact-producing container jobs, and agent-monitored long-running compute work.
69 changes: 69 additions & 0 deletions packages/components/nodes/tools/JungleGrid/core.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { JungleGridClient, createJungleGridTools } from './core'
import { secureAxiosRequest } from '../../../src/httpSecurity'

jest.mock('../../../src/httpSecurity', () => ({
secureAxiosRequest: jest.fn()
}))

const mockedSecureAxiosRequest = secureAxiosRequest as jest.MockedFunction<typeof secureAxiosRequest>

describe('JungleGridClient', () => {
beforeEach(() => {
mockedSecureAxiosRequest.mockReset()
mockedSecureAxiosRequest.mockResolvedValue({
status: 200,
data: { ok: true }
} as any)
})

it('uses bearer authentication and the documented estimate route', async () => {
const client = new JungleGridClient({ apiKey: 'test-key', baseUrl: 'https://api.junglegrid.dev/' })

await client.estimateJob({ workload_type: 'inference', image: 'python:3.11' })

expect(mockedSecureAxiosRequest).toHaveBeenCalledWith(
expect.objectContaining({
url: 'https://api.junglegrid.dev/v1/jobs/estimate',
method: 'POST',
headers: expect.objectContaining({
Authorization: 'Bearer test-key',
'Content-Type': 'application/json'
})
})
)
})

it('uses verified production routes for lifecycle and artifact operations', async () => {
const client = new JungleGridClient({ apiKey: 'test-key' })

await client.submitJob({ workload_type: 'batch', image: 'python:3.11', command: 'python', args: ['-c', 'print(42)'] })
await client.listJobs({ limit: 20, status: 'running' })
await client.getJob('job_123')
await client.getJobRuntime('job_123')
await client.cancelJob('job_123', 'test')
await client.listJobArtifacts('job_123')
await client.getArtifactDownloadUrl('job_123', 'artifact_123')

const urls = mockedSecureAxiosRequest.mock.calls.map(([config]) => config.url)
expect(urls).toEqual([
'https://api.junglegrid.dev/v1/jobs',
'https://api.junglegrid.dev/v1/jobs?limit=20&status=running',
'https://api.junglegrid.dev/v1/jobs/job_123',
'https://api.junglegrid.dev/v1/jobs/job_123/runtime',
'https://api.junglegrid.dev/v1/jobs/job_123/cancel',
'https://api.junglegrid.dev/v1/jobs/job_123/artifacts',
'https://api.junglegrid.dev/v1/jobs/job_123/artifacts/artifact_123/download'
])
})
})

describe('createJungleGridTools', () => {
it('creates agent-facing tools with async job guidance', () => {
const client = new JungleGridClient({ apiKey: 'test-key' })
const tools = createJungleGridTools(client, ['estimateJob', 'submitJob', 'getJob'])

expect(tools.map((tool) => tool.name)).toEqual(['jungle_grid_estimate_job', 'jungle_grid_submit_job', 'jungle_grid_get_job'])
expect(tools[1].description).toContain('returns a job_id immediately')
expect(tools[1].description).toContain('does not mean the job has finished')
})
})
Loading