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 }
74 changes: 74 additions & 0 deletions packages/components/nodes/tools/JungleGrid/JungleGrid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { secureAxiosRequest } from '../../../src/httpSecurity'
import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam } from '../../../src/utils'

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

jest.mock('../../../src/utils', () => ({
convertMultiOptionsToStringArray: jest.fn((value) => (Array.isArray(value) ? value : value ? [value] : [])),
getCredentialData: jest.fn(),
getCredentialParam: jest.fn((name, credentialData) => credentialData[name])
}))

const mockedSecureAxiosRequest = secureAxiosRequest as jest.MockedFunction<typeof secureAxiosRequest>
const mockedGetCredentialData = getCredentialData as jest.MockedFunction<typeof getCredentialData>
const mockedGetCredentialParam = getCredentialParam as jest.MockedFunction<typeof getCredentialParam>
const mockedConvertMultiOptionsToStringArray = convertMultiOptionsToStringArray as jest.MockedFunction<
typeof convertMultiOptionsToStringArray
>

describe('JungleGrid_Tools', () => {
beforeEach(() => {
mockedSecureAxiosRequest.mockReset()
mockedSecureAxiosRequest.mockResolvedValue({
status: 200,
data: { available: true }
} as any)
mockedGetCredentialData.mockReset()
mockedGetCredentialParam.mockClear()
mockedConvertMultiOptionsToStringArray.mockClear()
})

it('loads Jungle Grid credentials through Flowise credential helpers', async () => {
mockedGetCredentialData.mockResolvedValue({
apiKey: 'credential-api-key',
baseUrl: 'https://api.example.test/'
} as any)

const { nodeClass } = require('./JungleGrid')
const node = new nodeClass()
const tools = await node.init(
{
credential: 'credential-id',
inputs: {
actions: ['estimateJob']
}
} as any,
'',
{} as any
)

await tools[0]._call({ workload_type: 'batch', image: 'python:3.11' })

expect(mockedGetCredentialData).toHaveBeenCalledWith('credential-id', {})
expect(mockedGetCredentialParam).toHaveBeenCalledWith(
'apiKey',
expect.objectContaining({ apiKey: 'credential-api-key' }),
expect.anything()
)
expect(mockedGetCredentialParam).toHaveBeenCalledWith(
'baseUrl',
expect.objectContaining({ baseUrl: 'https://api.example.test/' }),
expect.anything()
)
expect(mockedSecureAxiosRequest).toHaveBeenCalledWith(
expect.objectContaining({
url: 'https://api.example.test/v1/jobs/estimate',
headers: expect.objectContaining({
Authorization: 'Bearer credential-api-key'
})
})
)
})
})
69 changes: 69 additions & 0 deletions packages/components/nodes/tools/JungleGrid/JungleGrid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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 Artifacts', name: 'listArtifacts' },
{ label: 'Get Artifact', name: 'getArtifact' }
]

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/api'
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', 'listArtifacts', 'getArtifact'],
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 }
138 changes: 138 additions & 0 deletions packages/components/nodes/tools/JungleGrid/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Jungle Grid

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

Jungle Grid is live production infrastructure. Submitting jobs may start managed compute and spend credits. Estimate first whenever a user needs cost, capacity, or routing information before execution.

## Links

- Website: https://junglegrid.dev
- Docs: https://junglegrid.dev/docs
- API docs: https://junglegrid.dev/docs/api
- MCP docs: https://junglegrid.dev/docs/mcp
- MCP server: https://github.com/Jungle-Grid/mcp-server
- Discord: https://discord.com/invite/kpJqxXFFCs

## Credentials

Create a `Jungle Grid API` credential in Flowise:

- `Jungle Grid API Key`: a scoped Jungle Grid API key from the Jungle Grid portal.
- `Jungle Grid API Base URL`: defaults to `https://api.junglegrid.dev`. Override only for staging, local development, or private deployments.

Keep the API key in Flowise credentials or a server-side environment variable used to create that credential. Do not place Jungle Grid API keys in prompts, command arguments, exported flows, source code, logs, browser code, or public repositories.

Recommended API key scopes:

- `jobs:estimate` for estimates.
- `jobs:submit` or `jobs:write` for submit and cancel.
- `jobs:read` for list, status, runtime, logs, and artifacts.
- `logs:read` for stored job logs.

## Actions

- `Estimate Job`: calls `POST /v1/jobs/estimate`. This is read-only and does not start compute.
- `Submit Job`: calls `POST /v1/jobs`. This starts asynchronous remote work and can spend credits.
- `List Jobs`: calls `GET /v1/jobs` with `limit`, `cursor`, and `status` filters.
- `Get Job`: calls `GET /v1/jobs/{job_id}` for status and job details.
- `Get Job Runtime`: calls `GET /v1/jobs/{job_id}/runtime` for runtime tails, exit code, timeout, and diagnostics where available.
- `Get Job Logs`: calls `GET /v1/jobs/{job_id}/logs` with `tail`, `limit`, `cursor`, and `stream` filters.
- `Cancel Job`: calls `POST /v1/jobs/{job_id}/cancel`. Use only for non-terminal jobs.
- `List Artifacts`: calls `GET /v1/jobs/{job_id}/artifacts`.
- `Get Artifact`: calls `POST /v1/jobs/{job_id}/artifacts/{artifact_id}/download` to create temporary download information.

Live log streaming uses `GET /v1/jobs/{job_id}/logs/live` as Server-Sent Events. Flowise tools execute synchronously, so this integration exposes polling through `Get Job` and `Get Job Logs` instead of holding a long-lived stream.

## Usage Pattern

```text
Estimate Job
-> review estimate/capacity/cost fields
-> Submit Job after user approval
-> store job_id
-> Get Job / Get Job Runtime / Get Job Logs
-> wait for completed, failed, rejected, or cancelled
-> List Artifacts
-> Get Artifact
```

`Submit Job` returns a `job_id` immediately. It does not wait for completion. Poll until Jungle Grid reports a terminal status before assuming outputs are final.

## Example Workloads

Inference estimate:

```json
{
"name": "flowise-inference-estimate",
"image": "python:3.11",
"workload_type": "inference",
"model_size_gb": 1,
"optimize_for": "balanced"
}
```

Low-cost batch submit:

```json
{
"name": "flowise-batch-smoke",
"image": "python:3.11",
"workload_type": "batch",
"model_size_gb": 1,
"optimize_for": "cost",
"command": "python",
"args": ["-c", "print(42)"]
}
```

Agent-triggered artifact job:

```json
{
"name": "flowise-artifact-job",
"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'))"
]
}
```

Job monitoring:

```json
{
"job_id": "job_..."
}
```

Logs retrieval:

```json
{
"job_id": "job_...",
"tail": 100,
"stream": "all"
}
```

Artifact retrieval:

```json
{
"job_id": "job_...",
"artifact_id": "art_..."
}
```

## Field Notes

- `workload_type` supports `inference`, `training`, `fine_tuning` / `fine-tuning`, and `batch`. The integration sends `fine-tuning` to the REST API.
- `routing_mode` is accepted as an agent-friendly alias for `optimize_for`.
- `env` is accepted as an alias for `environment`; values must be strings.
- `callback_url`, `callback_auth_token`, and `callback_metadata` follow the documented per-job callback fields. Treat callback tokens as secrets.
- Managed jobs can upload regular files written under `/workspace/artifacts`. Direct input file upload is not part of the documented public job-submit contract; pass file references through your image, command, environment, or storage system where appropriate.
Loading