From 3bf88f8d935e80113dba6864233e6fb4f8c943ca Mon Sep 17 00:00:00 2001 From: mich-elle-luna Date: Wed, 27 May 2026 13:02:32 -0700 Subject: [PATCH] Add Node.js agent templates and unlock JavaScript in agent builder 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 --- content/develop/ai/agent-builder/_index.md | 2 +- .../javascript/conversational_agent.js | 251 ++++++++++++ .../javascript/recommendation_agent.js | 363 ++++++++++++++++++ static/js/agent-builder.js | 14 +- 4 files changed, 622 insertions(+), 8 deletions(-) create mode 100644 static/code/agent-templates/javascript/conversational_agent.js create mode 100644 static/code/agent-templates/javascript/recommendation_agent.js diff --git a/content/develop/ai/agent-builder/_index.md b/content/develop/ai/agent-builder/_index.md index 8b27ce90fa..87bfea9d7e 100644 --- a/content/develop/ai/agent-builder/_index.md +++ b/content/develop/ai/agent-builder/_index.md @@ -39,7 +39,7 @@ The agent builder will generate complete, working code examples for your chosen ## Features -- **Multiple programming languages**: Generate code in Python, with JavaScript (Node.js), Java, and C# coming soon +- **Multiple programming languages**: Generate code in Python and JavaScript (Node.js), with Java and C# coming soon - **LLM integration**: Support for OpenAI, Anthropic Claude, and Llama 2 - **Redis optimized**: Uses Redis data structures for optimal performance diff --git a/static/code/agent-templates/javascript/conversational_agent.js b/static/code/agent-templates/javascript/conversational_agent.js new file mode 100644 index 0000000000..daaf314426 --- /dev/null +++ b/static/code/agent-templates/javascript/conversational_agent.js @@ -0,0 +1,251 @@ +/* + * Redis Conversational Agent (Node.js) + * Uses node-redis with Redis Search for semantic message history + * + * Requires Redis Stack 6.2+ or Redis 8 with the Search module for JSON + * vector indexing. The vector field is stored as a JSON array of floats, + * which is the correct on-disk format for JSON-backed vector indexes. + * + * To run this code: + * Install dependencies: + * npm install redis openai dotenv + * + * Set environment variables: + * LLM_API_KEY=your_${formData.llmModel.toLowerCase()}_api_key + * LLM_API_BASE_URL=your_base_url (optional, default: ${CONFIG.models[formData.llmModel].baseUrl}) + * LLM_MODEL=your_model_name (optional, default: ${CONFIG.models[formData.llmModel].defaultModel}) + * EMBEDDING_MODEL=your_embed_model (optional, default: text-embedding-3-small) + * VECTOR_DIM=1536 (optional, must match your embedding model's output dimension) + * REDIS_URL=redis://localhost:6379 + * (or use REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, REDIS_USERNAME separately) + */ + +require('dotenv').config(); +const { createClient } = require('redis'); +const OpenAI = require('openai'); + +const INDEX_NAME = 'message_history_idx'; +const MESSAGE_PREFIX = 'message:'; +const RECENT_KEY = (session) => `recent:${session}`; +const EMBEDDING_MODEL = process.env.EMBEDDING_MODEL || 'text-embedding-3-small'; +const VECTOR_DIM = parseInt(process.env.VECTOR_DIM) || 1536; +const RECENT_WINDOW = 6; // always include this many recent turns in context +const SEMANTIC_TOP_K = 4; // additional turns retrieved by semantic similarity +const MAX_CONTENT_CHARS = 2000; + +class ConversationalAgent { + constructor(sessionName = 'chat') { + this.sessionName = sessionName; + this.messageCount = 0; + this._dimValidated = false; + + this.llmApiKey = process.env.LLM_API_KEY; + if (!this.llmApiKey) throw new Error('LLM_API_KEY environment variable is required'); + + this.llmBaseUrl = process.env.LLM_API_BASE_URL || '${CONFIG.models[formData.llmModel].baseUrl}'; + this.llmModel = process.env.LLM_MODEL || '${CONFIG.models[formData.llmModel].defaultModel}'; + + this.openai = new OpenAI({ apiKey: this.llmApiKey, baseURL: this.llmBaseUrl }); + this.redisClient = null; + } + + async connect() { + const clientOptions = process.env.REDIS_URL + ? { url: process.env.REDIS_URL } + : { + socket: { + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT) || 6379, + }, + password: process.env.REDIS_PASSWORD || undefined, + username: process.env.REDIS_USERNAME || 'default', + }; + + this.redisClient = createClient(clientOptions); + this.redisClient.on('error', (err) => console.error('Redis error:', err)); + await this.redisClient.connect(); + console.log('Connected to Redis successfully'); + + await this._ensureIndex(); + console.log('LLM configured:', this.llmModel); + console.log('Embedding model:', EMBEDDING_MODEL, `(VECTOR_DIM=${VECTOR_DIM})`); + } + + async _ensureIndex() { + try { + await this.redisClient.ft.info(INDEX_NAME); + } catch { + await this.redisClient.ft.create( + INDEX_NAME, + { + '$.role': { type: 'TAG', AS: 'role' }, + '$.content': { type: 'TEXT', AS: 'content' }, + '$.session': { type: 'TAG', AS: 'session' }, + '$.embedding': { + type: 'VECTOR', + AS: 'embedding', + ALGORITHM: 'FLAT', + TYPE: 'FLOAT32', + DIM: VECTOR_DIM, + DISTANCE_METRIC: 'COSINE', + }, + }, + { ON: 'JSON', PREFIX: MESSAGE_PREFIX } + ); + console.log('Created search index:', INDEX_NAME); + } + } + + async _embed(text) { + const response = await this.openai.embeddings.create({ + model: EMBEDDING_MODEL, + input: text, + }); + const embedding = response.data[0].embedding; + + // Validate dimension on first call. If this throws, either set VECTOR_DIM + // to the correct value in your environment, or recreate the index. + if (!this._dimValidated) { + if (embedding.length !== VECTOR_DIM) { + throw new Error( + `Embedding model '${EMBEDDING_MODEL}' returned ${embedding.length} dimensions ` + + `but VECTOR_DIM is ${VECTOR_DIM}. ` + + `Set VECTOR_DIM=${embedding.length} and recreate the index.` + ); + } + this._dimValidated = true; + } + + return embedding; // plain JS number array + } + + _toQueryBuffer(embedding) { + return Buffer.from(new Float32Array(embedding).buffer); + } + + async _storeMessage(role, content) { + const truncated = content.slice(0, MAX_CONTENT_CHARS); + const embedding = await this._embed(truncated); + const key = `${MESSAGE_PREFIX}${this.sessionName}:${Date.now()}_${this.messageCount++}`; + + await this.redisClient.json.set(key, '$', { + role, + content: truncated, + session: this.sessionName, + embedding, // stored as JSON array of floats, required for JSON vector index + }); + + // 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); + } + + async _getRecentMessages() { + const keys = await this.redisClient.lRange(RECENT_KEY(this.sessionName), 0, -1); + if (!keys.length) return []; + const docs = await this.redisClient.json.mGet(keys, '$'); + return docs + .filter(Boolean) + .flatMap((d) => d) + .filter(Boolean) + .map((m) => ({ role: m.role, content: m.content, _key: m._key })); + } + + async _getSemanticMessages(query) { + const queryBuffer = this._toQueryBuffer(await this._embed(query)); + const results = await this.redisClient.ft.search( + INDEX_NAME, + `(@session:{${this.sessionName}})=>[KNN ${SEMANTIC_TOP_K} @embedding $vec AS score]`, + { + PARAMS: { vec: queryBuffer }, + RETURN: ['role', 'content', '__key'], + SORTBY: { BY: 'score', DIRECTION: 'ASC' }, + DIALECT: 2, + } + ); + return results.documents.map((doc) => ({ + role: doc.value.role, + content: doc.value.content, + _key: doc.id, + })); + } + + async _buildContext(userInput) { + // Hybrid: recent turns for conversational coherence + semantic search for deeper context. + const [recent, semantic] = await Promise.all([ + this._getRecentMessages().catch(() => []), + this._getSemanticMessages(userInput).catch(() => []), + ]); + + // Deduplicate by key, preserving recent turns first + const seen = new Set(recent.map((m) => m._key)); + const extra = semantic.filter((m) => !seen.has(m._key)); + + return [...recent, ...extra].map(({ role, content }) => ({ role, content })); + } + + async chat(userInput) { + const context = await this._buildContext(userInput); + + const messages = [ + { + role: 'system', + content: 'You are a helpful assistant that answers questions based on the conversation history.', + }, + ...context, + { role: 'user', content: userInput }, + ]; + + const response = await this.openai.chat.completions.create({ + model: this.llmModel, + messages, + }); + + const assistantResponse = response.choices[0]?.message?.content; + if (!assistantResponse) throw new Error('Empty response from LLM'); + + await this._storeMessage('user', userInput); + await this._storeMessage('assistant', assistantResponse); + + return assistantResponse; + } + + async disconnect() { + if (this.redisClient) await this.redisClient.disconnect(); + } +} + +async function main() { + const agent = new ConversationalAgent(); + try { + await agent.connect(); + console.log(await agent.chat('Tell me about yourself.')); + } catch (err) { + console.error('Failed to initialize agent:', err.message); + await agent.disconnect(); + process.exit(1); + } + + const readline = require('readline'); + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + + const askQuestion = () => { + rl.question('Enter a prompt: ', async (input) => { + if (['quit', 'exit', 'bye'].includes(input.toLowerCase())) { + console.log('Goodbye!'); + rl.close(); + await agent.disconnect(); + return; + } + try { + console.log(await agent.chat(input)); + } catch (err) { + console.error('Error:', err.message); + } + askQuestion(); + }); + }; + askQuestion(); +} + +main(); diff --git a/static/code/agent-templates/javascript/recommendation_agent.js b/static/code/agent-templates/javascript/recommendation_agent.js new file mode 100644 index 0000000000..fdc6eb5824 --- /dev/null +++ b/static/code/agent-templates/javascript/recommendation_agent.js @@ -0,0 +1,363 @@ +/* + * Redis Recommendation Engine (Node.js) + * Uses node-redis with Redis Search for movie recommendations + * + * To run this code: + * Install dependencies: + * npm install redis openai dotenv csv-parse + * + * Set environment variables: + * LLM_API_KEY=your_${formData.llmModel.toLowerCase()}_api_key + * LLM_API_BASE_URL=your_base_url (optional, default: ${CONFIG.models[formData.llmModel].baseUrl}) + * LLM_MODEL=your_model_name (optional, default: ${CONFIG.models[formData.llmModel].defaultModel}) + * REDIS_URL=redis://localhost:6379 + * (or use REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, REDIS_USERNAME separately) + * + * Download datasets: + * https://redis-ai-resources.s3.us-east-2.amazonaws.com/recommenders/datasets/collaborative-filtering/ratings_small.csv + * https://redis-ai-resources.s3.us-east-2.amazonaws.com/recommenders/datasets/collaborative-filtering/movies_metadata.csv + * Place them in datasets/collaborative_filtering/ relative to this file. + */ + +require('dotenv').config(); +const { createClient } = require('redis'); +const OpenAI = require('openai'); +const { parse } = require('csv-parse/sync'); +const fs = require('fs'); +const path = require('path'); + +const INDEX_NAME = 'movies_idx'; +const MOVIE_PREFIX = 'movie:'; + +const CONFIG = { + maxResults: 10, + defaultResults: 5, + minRevenueFilter: 30_000_000, + validSortFields: new Set(['popularityScore', 'avgRating', 'ratingCount', 'revenue']), + validSortOrders: new Set(['DESC', 'ASC']), +}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Parse genres from movies_metadata.csv. + * The field is stored as a Python-style list of dicts, e.g.: + * "[{'id': 28, 'name': 'Action'}, {'id': 12, 'name': 'Adventure'}]" + * Returns a comma-separated string for use as a Redis TAG field. + */ +function parseGenres(raw) { + if (!raw || raw === '[]') return ''; + try { + const json = raw.replace(/'/g, '"').replace(/None/g, 'null').replace(/True/g, 'true').replace(/False/g, 'false'); + const parsed = JSON.parse(json); + return parsed.map((g) => g?.name).filter(Boolean).join(','); + } catch { + console.warn('parseGenres: could not parse genres field, storing empty string. Raw value:', raw?.slice(0, 80)); + return ''; + } +} + +function safeNumber(value, fallback = 0) { + const n = Number(value); + return isFinite(n) ? n : fallback; +} + +/** + * Validate and sanitize LLM-returned query params. + * Rejects any field that doesn't match expected types or allowed values. + */ +function validateQueryParams(raw) { + return { + genres: Array.isArray(raw?.genres) + ? raw.genres.filter((g) => typeof g === 'string' && g.trim()) + : null, + minRating: typeof raw?.minRating === 'number' && raw.minRating >= 0 && raw.minRating <= 10 + ? raw.minRating + : null, + minReviews: typeof raw?.minReviews === 'number' && raw.minReviews > 0 + ? Math.floor(raw.minReviews) + : null, + maxResults: typeof raw?.maxResults === 'number' + ? Math.min(Math.max(1, Math.floor(raw.maxResults)), CONFIG.maxResults) + : CONFIG.defaultResults, + sortBy: CONFIG.validSortFields.has(raw?.sortBy) + ? raw.sortBy + : 'popularityScore', + sortOrder: CONFIG.validSortOrders.has(raw?.sortOrder?.toUpperCase?.()) + ? raw.sortOrder.toUpperCase() + : 'DESC', + revenueFilter: raw?.revenueFilter === true, + }; +} + +// --------------------------------------------------------------------------- +// Agent class +// --------------------------------------------------------------------------- + +class RecommendationAgent { + constructor() { + this.llmApiKey = process.env.LLM_API_KEY; + if (!this.llmApiKey) throw new Error('LLM_API_KEY environment variable is required'); + + this.llmBaseUrl = process.env.LLM_API_BASE_URL || '${CONFIG.models[formData.llmModel].baseUrl}'; + this.llmModel = process.env.LLM_MODEL || '${CONFIG.models[formData.llmModel].defaultModel}'; + + this.openai = new OpenAI({ apiKey: this.llmApiKey, baseURL: this.llmBaseUrl }); + this.redisClient = null; + this.indexReady = false; + } + + async connect() { + const clientOptions = process.env.REDIS_URL + ? { url: process.env.REDIS_URL } + : { + socket: { + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT) || 6379, + }, + password: process.env.REDIS_PASSWORD || undefined, + username: process.env.REDIS_USERNAME || 'default', + }; + + this.redisClient = createClient(clientOptions); + this.redisClient.on('error', (err) => console.error('Redis error:', err)); + await this.redisClient.connect(); + console.log('Connected to Redis successfully'); + console.log('LLM configured:', this.llmModel); + + await this._setupMovieIndex(); + } + + async _indexExists() { + try { + const info = await this.redisClient.ft.info(INDEX_NAME); + return parseInt(info.numDocs) > 0; + } catch { + return false; + } + } + + async _setupMovieIndex() { + // Skip loading if the index already exists and has documents. + if (await this._indexExists()) { + console.log('Movie index already loaded, skipping dataset import.'); + this.indexReady = true; + return; + } + + const ratingsFile = path.join('datasets', 'collaborative_filtering', 'ratings_small.csv'); + const moviesFile = path.join('datasets', 'collaborative_filtering', 'movies_metadata.csv'); + + if (!fs.existsSync(ratingsFile) || !fs.existsSync(moviesFile)) { + console.warn('Movie datasets not found. Skipping index setup.'); + console.warn(` Expected: ${ratingsFile}`); + console.warn(` Expected: ${moviesFile}`); + return; + } + + console.log('Loading movie datasets...'); + + let ratings, movies; + try { + ratings = parse(fs.readFileSync(ratingsFile), { columns: true, cast: true }); + movies = parse(fs.readFileSync(moviesFile), { columns: true, cast: true }); + } catch (err) { + console.error('Failed to parse dataset files:', err.message); + return; + } + + if (!ratings.length || !movies.length) { + console.error('One or more dataset files are empty.'); + return; + } + + // Aggregate ratings per movie + const stats = {}; + for (const r of ratings) { + if (!r.movieId || !isFinite(r.rating)) continue; + if (!stats[r.movieId]) stats[r.movieId] = { count: 0, total: 0 }; + stats[r.movieId].count++; + stats[r.movieId].total += r.rating; + } + + // Merge metadata with aggregated stats + const merged = movies + .filter((m) => m.id && stats[String(m.id)]) + .map((m) => { + const s = stats[String(m.id)]; + const avgRating = s.total / s.count; + return { + movieId: String(m.id), + title: String(m.title || '').trim(), + genres: parseGenres(m.genres), // comma-separated TAG string + revenue: safeNumber(m.revenue), + ratingCount: s.count, + avgRating: Math.round(avgRating * 100) / 100, + popularityScore: Math.round(s.count * avgRating * 100) / 100, + }; + }) + .filter((m) => m.title); + + if (!merged.length) { + console.error('No valid movies found after merging datasets.'); + return; + } + + console.log(`Processed ${merged.length} movies`); + + // Drop existing index if present, ignoring "index not found" errors only. + try { + await this.redisClient.ft.dropIndex(INDEX_NAME); + } catch (err) { + if (!err.message?.includes('Unknown Index name')) throw err; + } + + await this.redisClient.ft.create( + INDEX_NAME, + { + '$.movieId': { type: 'TAG', AS: 'movieId' }, + '$.title': { type: 'TEXT', AS: 'title' }, + '$.genres': { type: 'TAG', AS: 'genres', SEPARATOR: ',' }, + '$.revenue': { type: 'NUMERIC', AS: 'revenue' }, + '$.ratingCount': { type: 'NUMERIC', AS: 'ratingCount' }, + '$.avgRating': { type: 'NUMERIC', AS: 'avgRating' }, + '$.popularityScore': { type: 'NUMERIC', AS: 'popularityScore' }, + }, + { ON: 'JSON', PREFIX: MOVIE_PREFIX } + ); + + // Load using a pipeline for efficiency + const pipeline = this.redisClient.multi(); + for (const movie of merged) { + pipeline.json.set(`${MOVIE_PREFIX}${movie.movieId}`, '$', movie); + } + await pipeline.exec(); + + this.indexReady = true; + console.log('Movie recommendation system initialized successfully!'); + } + + async _parseUserQuery(userQuery) { + const systemPrompt = `You are a movie recommendation assistant. Parse the user's query and return a JSON object with: +- "genres": array of genre name strings or null +- "minRating": minimum average rating (0-10) or null +- "minReviews": minimum review count or null +- "maxResults": number of results (default 5, max 10) +- "sortBy": one of "popularityScore", "avgRating", "ratingCount", "revenue" +- "sortOrder": "DESC" or "ASC" +- "revenueFilter": true for blockbusters, null otherwise + +Return only valid JSON with no explanation or markdown.`; + + try { + const response = await this.openai.chat.completions.create({ + model: this.llmModel, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userQuery }, + ], + temperature: 0.1, + }); + + const raw = JSON.parse(response.choices[0]?.message?.content || '{}'); + return validateQueryParams(raw); // always returns a safe, validated object + } catch { + return validateQueryParams({}); // safe defaults on any failure + } + } + + async recommendMovies(userQuery) { + if (!this.indexReady) { + return 'The movie database is not available. Please check that the dataset files are present.'; + } + + const params = await this._parseUserQuery(userQuery); + + // Build filter parts — genres now filtered in Redis via TAG, not in JavaScript + const filterParts = []; + if (params.minRating) filterParts.push(`@avgRating:[${params.minRating} +inf]`); + if (params.minReviews) filterParts.push(`@ratingCount:[${params.minReviews} +inf]`); + if (params.revenueFilter) filterParts.push(`@revenue:[${CONFIG.minRevenueFilter} +inf]`); + if (params.genres?.length) { + // Redis TAG filter: match any of the requested genres + const tagList = params.genres.map((g) => g.replace(/[^a-zA-Z0-9 ]/g, '')).join('|'); + if (tagList) filterParts.push(`@genres:{${tagList}}`); + } + + const filterQuery = filterParts.length > 0 ? filterParts.join(' ') : '*'; + + let results; + try { + results = await this.redisClient.ft.search(INDEX_NAME, filterQuery, { + RETURN: ['title', 'genres', 'ratingCount', 'avgRating', 'popularityScore'], + SORTBY: { BY: params.sortBy, DIRECTION: params.sortOrder }, + LIMIT: { from: 0, size: params.maxResults }, + }); + } catch (err) { + console.error('Search error:', err.message); + return 'Sorry, there was an error searching the movie database.'; + } + + const movies = results?.documents?.map((d) => d.value).filter(Boolean) ?? []; + + if (!movies.length) { + return "Sorry, no movies found matching your criteria. Try adjusting your preferences."; + } + + let response = `Based on your request '${userQuery}', here are my recommendations:\n\n`; + movies.forEach((m, i) => { + response += `${i + 1}. ${m.title}\n`; + response += ` Genres: ${m.genres || 'N/A'}\n`; + response += ` Average Rating: ${parseFloat(m.avgRating || 0).toFixed(1)}/10 (${m.ratingCount || 0} reviews)\n`; + response += ` Popularity Score: ${parseFloat(m.popularityScore || 0).toFixed(1)}\n\n`; + }); + return response; + } + + async disconnect() { + if (this.redisClient) await this.redisClient.disconnect(); + } +} + +async function main() { + const agent = new RecommendationAgent(); + try { + await agent.connect(); + } catch (err) { + console.error('Failed to initialize agent:', err.message); + await agent.disconnect(); + process.exit(1); + } + + console.log('\nWelcome to the Redis Movie Recommendation Agent!'); + console.log("Ask for movie recommendations. Type 'quit' to exit.\n"); + console.log("Here's a quick demo:"); + console.log(await agent.recommendMovies('Show me some popular movies')); + + const readline = require('readline'); + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + + const askQuestion = () => { + rl.question('\nWhat kind of movies are you looking for? ', async (input) => { + if (['quit', 'exit', 'bye'].includes(input.toLowerCase())) { + console.log('Goodbye!'); + rl.close(); + await agent.disconnect(); + return; + } + if (input.trim()) { + try { + console.log('\n' + await agent.recommendMovies(input)); + } catch (err) { + console.error('Error:', err.message); + } + } + askQuestion(); + }); + }; + askQuestion(); +} + +main(); diff --git a/static/js/agent-builder.js b/static/js/agent-builder.js index 91f37ebee3..098a404687 100644 --- a/static/js/agent-builder.js +++ b/static/js/agent-builder.js @@ -426,8 +426,8 @@ } if (selectedLang) { - // Check if it's Python (fully supported) - if (selectedLang === 'python') { + // Check if it's a fully supported language + if (selectedLang === 'python' || selectedLang === 'javascript') { conversationState.selections.programmingLanguage = selectedLang; const config = CONFIG.languages[selectedLang]; @@ -445,9 +445,10 @@ const config = CONFIG.languages[selectedLang]; const languageName = config.name; - addMessage(`${languageName} support is coming soon. Currently, only Python is fully supported.`, 'bot'); - addMessage(`Would you like to build a Python agent instead?`, 'bot', [ - { value: 'python', label: 'Yes, use Python' }, + addMessage(`${languageName} support is coming soon. Currently, Python and JavaScript (Node.js) are fully supported.`, 'bot'); + addMessage(`Would you like to build an agent in a supported language instead?`, 'bot', [ + { value: 'python', label: 'Use Python' }, + { value: 'javascript', label: 'Use JavaScript (Node.js)' }, { value: 'wait', label: 'I\'ll wait for ' + languageName } ]); } @@ -520,8 +521,7 @@ java: '.java', csharp: '.cs' }; - const base = window.HUGO_BASEURL || ''; - const filename = `${base}code/agent-templates/${formData.programmingLanguage}/${formData.agentType}_agent${fileExtensions[formData.programmingLanguage]}`; + const filename = `/code/agent-templates/${formData.programmingLanguage}/${formData.agentType}_agent${fileExtensions[formData.programmingLanguage]}`; return loadTemplateFile(filename, formData) || genericTemplates[formData.programmingLanguage](formData); }