Skip to content

Add Node.js agent templates and unlock JavaScript in agent builder#3374

Open
mich-elle-luna wants to merge 1 commit into
mainfrom
agent-builder-node-js
Open

Add Node.js agent templates and unlock JavaScript in agent builder#3374
mich-elle-luna wants to merge 1 commit into
mainfrom
agent-builder-node-js

Conversation

@mich-elle-luna
Copy link
Copy Markdown
Collaborator

@mich-elle-luna mich-elle-luna commented May 27, 2026

Add conversational and recommendation agent templates using node-redis v4 with Redis Search for vector-based message history and movie indexing. Conversational agent uses hybrid recent+semantic context retrieval, runtime embedding dimension validation, and a clear note on Redis version requirements. Recommendation agent parses genres from MovieLens CSV format into Redis TAGs, validates LLM query params against an explicit allowlist, skips dataset reload if the index is already warm, and filters genres in Redis rather than JS. Fix template URL to use root-relative path so local templates load in dev. Update agent builder to support JavaScript alongside Python.


Note

Low Risk
Documentation, static example code, and client-side wizard/template path changes with no production auth or data-path changes.

Overview
This PR ships Node.js agent templates and treats JavaScript (Node.js) as a first-class language in the interactive agent builder, alongside Python.

New static templates under static/code/agent-templates/javascript/ cover conversational and recommendation agents using node-redis and Redis Search. The conversational sample stores JSON message history with vector embeddings, validates embedding dimensions at runtime, and builds context from recent turns plus semantic KNN. The recommendation sample loads MovieLens-style CSVs, normalizes genres for Redis TAG filters, sanitizes LLM-parsed query params, skips re-import when the index is already populated, and runs genre/rating filters in Redis rather than in application code.

static/js/agent-builder.js now advances the wizard for javascript (not only Python), updates “coming soon” copy and fallback chips to offer Python or Node.js, and fixes template loading to a root-relative /code/agent-templates/... path so local dev can fetch templates without HUGO_BASEURL.

Docs on the agent builder page now state that code can be generated in Python and JavaScript, with Java and C# still listed as coming soon.

Reviewed by Cursor Bugbot for commit 3bf88f8. Bugbot is set up for automated code reviews on this repo. Configure here.

Add conversational and recommendation agent templates using node-redis v4
with Redis Search for vector-based message history and movie indexing.
Conversational agent uses hybrid recent+semantic context retrieval, runtime
embedding dimension validation, and a clear note on Redis version requirements.
Recommendation agent parses genres from MovieLens CSV format into Redis TAGs,
validates LLM query params against an explicit allowlist, skips dataset reload
if the index is already warm, and filters genres in Redis rather than JS.
Fix template URL to use root-relative path so local templates load in dev.
Update agent builder to support JavaScript alongside Python.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

@mich-elle-luna mich-elle-luna requested a review from a team May 27, 2026 20:22
@jit-ci
Copy link
Copy Markdown

jit-ci Bot commented May 27, 2026

🛡️ Jit Security Scan Results

CRITICAL HIGH MEDIUM

✅ No security findings were detected in this PR


Security scan by Jit

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3bf88f8. Configure here.

.flatMap((d) => d)
.filter(Boolean)
.map((m) => ({ role: m.role, content: m.content, _key: m._key }));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deduplication broken because _key is always undefined

High Severity

In _getRecentMessages, m._key reads from the JSON document retrieved from Redis, but _storeMessage only stores { role, content, session, embedding } — there is no _key field. So m._key is always undefined for every recent message. In _buildContext, the seen set becomes Set([undefined]), meaning no semantic results are ever filtered out. The deduplication between recent and semantic messages is completely non-functional, causing duplicate messages in the LLM context. The keys array from lRange holds the actual Redis keys but is never mapped onto the returned objects.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3bf88f8. Configure here.


// Track insertion order for recent-turn retrieval
await this.redisClient.rPush(RECENT_KEY(this.sessionName), key);
await this.redisClient.lTrim(RECENT_KEY(this.sessionName), -RECENT_WINDOW * 4, -1);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recent window retrieval ignores RECENT_WINDOW limit

Low Severity

RECENT_WINDOW is 6, described as "always include this many recent turns in context," but lTrim uses RECENT_WINDOW * 4 (keeping 24 items, i.e. 12 turns), and _getRecentMessages fetches all items via lRange(0, -1). The effective recent window is 12 turns — double the stated intent. The multiplier likely needs to be * 2 (user + assistant per turn), and/or retrieval needs to be bounded.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3bf88f8. Configure here.

// Check if it's Python (fully supported)
if (selectedLang === 'python') {
// Check if it's a fully supported language
if (selectedLang === 'python' || selectedLang === 'javascript') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jupyter button misleadingly enabled for JavaScript agents

Low Severity

By adding 'javascript' as a fully supported language, JavaScript + OpenAI users now reach displayGeneratedCode, which only checks formData.llmModel !== 'openai' when deciding whether to enable the Jupyter button. A JavaScript + OpenAI user sees an active "Try in Jupyter" button, but clicking it falls through to a "coming soon" alert since tryInJupyter requires programmingLanguage === 'python'. The button enable/disable logic needs to also gate on the language being Python.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3bf88f8. Configure here.

Copy link
Copy Markdown
Collaborator

@dwdougherty dwdougherty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, but I think somebody with more Node.js experience should take a look. I'll add Docs back into the review fray.

@dwdougherty dwdougherty requested a review from a team May 27, 2026 21:13
@andy-stark-redis
Copy link
Copy Markdown
Contributor

I got Claude to have a look and he found the following:

Agent builder — JavaScript generation review

Findings from auditing the JavaScript code generated by the agent builder on the Agent builder page. Templates live in static/code/agent-templates/javascript/; the substitution logic is agent-builder.js:529-560.

What was checked: syntax of all 6 generated outputs (2 agent types × 3 models), template substitution correctness, Python ↔ JS parity, and runtime behaviour against Redis 8.2.1 (RediSearch + ReJSON loaded) with the OpenAI SDK stubbed.

What was not checked: real LLM calls, the Anthropic and Llama3 variants end-to-end, the full MovieLens dataset (used a 5-movie synthetic seed), or non-OpenAI embedding behaviour.

Critical — blocks fresh installs

1. ft.dropIndex error-string match is obsolete

recommendation_agent.js:210-215

try {
  await this.redisClient.ft.dropIndex(INDEX_NAME);
} catch (err) {
  if (!err.message?.includes('Unknown Index name')) throw err;
}

Current Redis (verified on 8.2.1 via redis-cli FT.DROPINDEX nonexistent_idx) returns "<name>: no such index", not "Unknown Index name". On a fresh install where movies_idx doesn't exist yet, the catch re-throws and the agent crashes during connect() before any data is loaded.

Fix sketch: match the modern wording too, or replace the string check with a structural test:

const exists = await this.redisClient.ft.info(INDEX_NAME).then(() => true).catch(() => false);
if (exists) await this.redisClient.ft.dropIndex(INDEX_NAME);

2. info.numDocs should be info.num_docs

recommendation_agent.js:133-140

async _indexExists() {
  try {
    const info = await this.redisClient.ft.info(INDEX_NAME);
    return parseInt(info.numDocs) > 0;
  } catch { return false; }
}

ft.info() in node-redis returns the field as num_docs (server-native, snake_case). parseInt(undefined) > 0 is always false, so _indexExists() always returns false. The "skip dataset import if already loaded" shortcut in _setupMovieIndex() never fires — every startup reloads the entire MovieLens dataset and rewrites every key, including dropping and recreating the index.

This compounds with bug 1: even after bug 1 is fixed, restarts will redo all the data loading work unnecessarily.

Fix: return parseInt(info.num_docs) > 0;

High — silent wrong behaviour

3. Anthropic variant uses the OpenAI SDK against api.anthropic.com

Both conversational_agent.js:48 and recommendation_agent.js:107 do:

this.openai = new OpenAI({ apiKey: this.llmApiKey, baseURL: this.llmBaseUrl });

For the anthropic variant, baseURL is substituted to https://api.anthropic.com/v1/, which is not OpenAI-protocol-compatible. Requests will fail at runtime. The Llama3 variant happens to work because Ollama exposes an OpenAI-compatible endpoint, but anyone selecting Anthropic in the builder gets non-working code.

Additionally, conversational_agent.js:100-104 hard-codes text-embedding-3-small and VECTOR_DIM=1536 regardless of which model the user selected. Even with a working chat-completions endpoint, embeddings require OpenAI.

Options:

  • Remove Anthropic from the JS language matrix until the templates use @anthropic-ai/sdk.
  • Or route Anthropic to its own SDK and keep embeddings on a dedicated provider.
  • Or document that JS requires an OpenAI-compatible endpoint for both chat and embeddings (and adjust the model selector accordingly).

Medium — misleading code

4. Recent-window arithmetic in conversational agent

conversational_agent.js:32 declares const RECENT_WINDOW = 6 with the comment "always include this many recent turns in context". But line 140 trims with -RECENT_WINDOW * 4 (= 24 items), and line 144 reads 0, -1 (everything in the list). Actual recent context is up to 24 messages, not 6.

The * 4 looks like leftover from an earlier design (perhaps "4 sessions × N messages"). Either drop the * 4 so behaviour matches the constant, or rename the constant to reflect the real window size.

Low — UX and consistency

5. Llama3 variant still requires LLM_API_KEY

Both templates throw if LLM_API_KEY is unset, but local Ollama doesn't need one. A user picking Llama3 has to set a dummy value, and the header line LLM_API_KEY=your_llama3_api_key reinforces the wrong expectation.

Fix: in the Llama3 variant, either skip the throw when baseUrl is localhost, or rephrase the header comment to say something like "(any non-empty value works for local Ollama)".

6. Python ↔ JS parity drift

The two languages are independent reimplementations, not ports. Implications worth deciding on (and ideally documenting on the agent builder page):

  • Conversational: Python uses RedisVL's SemanticMessageHistory; JS hand-rolls an FT index. Index names, key prefixes, field names, and retrieval strategy (pure-semantic with threshold vs. hybrid recency+KNN with no threshold) all differ. A user running both languages against the same Redis sees two parallel datasets that don't share data.
  • Recommendation: field names diverge (rating_count vs ratingCount, etc.); LLM JSON contract differs (snake_case vs camelCase, lowercase vs uppercase sort order, sentinel string vs boolean for revenue_filter); Python does no LLM-output validation while JS validates types/ranges/allowed values; Python filters genres in post-processing (because it stores the raw Python-literal blob) while JS pre-parses genres and filters server-side via TAG.
  • The JS recommendation agent is arguably better on a few axes (server-side genre filter, output validation). Worth deciding whether to back-port to Python or just call out the divergence on the docs page.

7. Generic-fallback drift

The fallback at agent-builder.js:599-640 — only reached if the static template file 404s — disagrees with the real templates:

  • Env var: fallback uses OPENAI_API_KEY / ANTHROPIC_API_KEY / LLAMA3_API_KEY; real templates use the model-agnostic LLM_API_KEY.
  • node-redis API shape: fallback uses v3-style { host, port } top-level; real templates use v4-style { socket: { host, port } }.

Not a today-bug, but a quiet rot-trap if template paths ever change.

Not bugs (avoid chasing)

  • node --check passes on all 6 generated outputs; no unresolved ${...} builder tokens leak through.
  • Imports and the npm install … comment line match in both templates.
  • node-redis v5 works with the templates even though comments imply v4 (the API surface used is preserved).
  • Server-side TAG filtering on multi-word genres ("Science Fiction") works as expected against Redis 8 — the simple regex sanitiser doesn't break them in practice.
  • The vector encoding (JSON array of floats for storage on a JSON-backed index, Buffer.from(Float32Array.buffer) for KNN query parameters) is the correct format and matches node-redis expectations.

Suggested fix order

  1. Bug 1 (dropIndex catch) — one-line change, unblocks fresh installs.
  2. Bug 2 (num_docs) — one-line change, stops needless reloads.
  3. Decide on bug 3 (Anthropic): remove from JS matrix, or properly support it.
  4. Bug 4 (recent-window) — clarify intent.
  5. Decide on bug 6 (parity vs documented divergence) — affects what the docs page should claim.
  6. Bug 5 (Llama3 key) and bug 7 (fallback drift) — cosmetic but easy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants