From 5b0d3050a34327922b1e759396393db94ffc2e7e Mon Sep 17 00:00:00 2001 From: Chris Cole Date: Mon, 30 Jun 2025 15:47:15 +0100 Subject: [PATCH 1/4] feat: 576858 - Use LLM for generating short description --- .../client/src/javascripts/preview/lib/llm.js | 13 + .../src/javascripts/preview/question.js | 17 + designer/server/src/config.ts | 27 + designer/server/src/lib/llm.js | 17 + designer/server/src/routes/ai/api.js | 43 ++ designer/server/src/routes/ai/index.js | 3 + designer/server/src/routes/index.js | 3 +- designer/server/src/service/langchain.js | 73 +++ .../src/form/form-editor/preview/question.js | 9 + package-lock.json | 513 +++++++++++++++++- package.json | 6 + 11 files changed, 719 insertions(+), 5 deletions(-) create mode 100644 designer/client/src/javascripts/preview/lib/llm.js create mode 100644 designer/server/src/lib/llm.js create mode 100644 designer/server/src/routes/ai/api.js create mode 100644 designer/server/src/routes/ai/index.js create mode 100644 designer/server/src/service/langchain.js diff --git a/designer/client/src/javascripts/preview/lib/llm.js b/designer/client/src/javascripts/preview/lib/llm.js new file mode 100644 index 0000000000..cadd6c3fd0 --- /dev/null +++ b/designer/client/src/javascripts/preview/lib/llm.js @@ -0,0 +1,13 @@ +export async function postShortDescription(title) { + const payload = JSON.stringify({ title }) + + const response = await window.fetch( + 'http://localhost:3000/api/ai/short_description', + { + body: payload, + method: 'POST', + headers: { 'Content-Type': 'application/json' } + } + ) + return response.json() +} diff --git a/designer/client/src/javascripts/preview/question.js b/designer/client/src/javascripts/preview/question.js index d043cc8806..a927229f50 100644 --- a/designer/client/src/javascripts/preview/question.js +++ b/designer/client/src/javascripts/preview/question.js @@ -1,4 +1,5 @@ import { DomElements } from '~/src/javascripts/preview/dom-elements.js' +import { postShortDescription } from '~/src/javascripts/preview/lib/llm.js' /** * @class QuestionDomElements @@ -160,6 +161,21 @@ export class EventListeners { 'input' ]) + const shortDescriptionText = /** @type {ListenerRow} */ ([ + this.baseElements.question, + /** + * @param {HTMLInputElement} target + */ + (target) => { + postShortDescription(target.value).then(({ shortDescription }) => { + this.baseElements.shortDesc.value = shortDescription + const event = new InputEvent('input', { bubbles: true }) + this.baseElements.shortDesc.dispatchEvent(event) + }) + }, + 'blur' + ]) + const hintText = /** @type {ListenerRow} */ ([ this.baseElements.hintText, /** @@ -185,6 +201,7 @@ export class EventListeners { questionText, hintText, optionalCheckbox, + shortDescriptionText, ...this.highlightListeners, ...this._customListeners ] diff --git a/designer/server/src/config.ts b/designer/server/src/config.ts index e6fc6e41b9..bf37688ddb 100644 --- a/designer/server/src/config.ts +++ b/designer/server/src/config.ts @@ -50,6 +50,15 @@ export interface Config { tracing: { header: string } + ai: { + llmEndpoint: string + anthropic: { + endpoint: string + key: string + version: string + maxTokens: number + } + } } // Define config schema @@ -156,6 +165,16 @@ const schema = joi.object({ roleEditorGroupId: joi.string().required(), tracing: joi.object({ header: joi.string().default('x-cdp-request-id') + }), + ai: joi.object({ + llmEndpoint: joi.string().required(), + anthropic: joi.object({ + maxTokens: joi.number().default(1024), + model: joi.string().default(''), + endpoint: joi.string().default('https://api.anthropic.com'), + key: joi.string().required(), + version: joi.string().default('2023-06-01') + }) }) }) @@ -195,6 +214,14 @@ const result = schema.validate( roleEditorGroupId: process.env.ROLE_EDITOR_GROUP_ID, tracing: { header: process.env.TRACING_HEADER + }, + ai: { + llmEndpoint: process.env.AI_LLM_ENDPOINT, + anthropic: { + endpoint: process.env.AI_ANTHROPIC_ENDPOINT, + key: process.env.AI_ANTHROPIC_KEY, + version: process.env.AI_ANTHROPIC_VERSION + } } }, { abortEarly: false } diff --git a/designer/server/src/lib/llm.js b/designer/server/src/lib/llm.js new file mode 100644 index 0000000000..fbfe452c45 --- /dev/null +++ b/designer/server/src/lib/llm.js @@ -0,0 +1,17 @@ +import config from '~/src/config.js' +import { postJson } from '~/src/lib/fetch.js' + +const llmEndpoint = new URL('/api/short_description', config.ai.llmEndpoint) + +export async function getShortDescription(title) { + const postJsonByType = + /** @type { typeof postJson<{ short_description: string }>} */ (postJson) + + const { body } = await postJsonByType(llmEndpoint, { + payload: { + title + } + }) + + return body +} diff --git a/designer/server/src/routes/ai/api.js b/designer/server/src/routes/ai/api.js new file mode 100644 index 0000000000..521f7649ce --- /dev/null +++ b/designer/server/src/routes/ai/api.js @@ -0,0 +1,43 @@ +import { StatusCodes } from 'http-status-codes' + +import * as scopes from '~/src/common/constants/scopes.js' +import { anthropicGetShortDescription } from '~/src/service/langchain.js' +export default [ + /** + * @satisfies {ServerRoute<{ Params: FormByIdInput }>} + */ + ({ + method: 'POST', + path: '/api/ai/short_description', + options: { + async handler(request, h) { + const { title } = request.payload + + try { + // const { short_description: shortDescription } = + // await getShortDescription(title) + // const shortDescription = await langchainJS(title) + const shortDescription = await anthropicGetShortDescription(title) + + return h.response({ shortDescription }).code(StatusCodes.OK) + } catch (e) { + console.error(e) + throw e + } + }, + auth: { + mode: 'required', + access: { + entity: 'user', + scope: [`+${scopes.SCOPE_WRITE}`] + } + } + } + }) +] + +/** + * @import { FormByIdInput, FormDefinition } from '@defra/forms-model' + * @import { ServerRoute } from '@hapi/hapi' + * @import { Level, SerializedError } from 'pino' + */ diff --git a/designer/server/src/routes/ai/index.js b/designer/server/src/routes/ai/index.js new file mode 100644 index 0000000000..5a630c292b --- /dev/null +++ b/designer/server/src/routes/ai/index.js @@ -0,0 +1,3 @@ +import ai from '~/src/routes/ai/api.js' + +export default [ai] diff --git a/designer/server/src/routes/index.js b/designer/server/src/routes/index.js index 740863324f..1ae7fab064 100644 --- a/designer/server/src/routes/index.js +++ b/designer/server/src/routes/index.js @@ -1,4 +1,5 @@ import account from '~/src/routes/account/index.js' +import ai from '~/src/routes/ai/api.js' import assets from '~/src/routes/assets.js' import file from '~/src/routes/file/file.js' import forms from '~/src/routes/forms/index.js' @@ -6,4 +7,4 @@ import health from '~/src/routes/health.js' import help from '~/src/routes/help.js' import home from '~/src/routes/home.js' -export default [account, assets, forms, health, home, help, file].flat() +export default [account, assets, forms, health, home, help, file, ai].flat() diff --git a/designer/server/src/service/langchain.js b/designer/server/src/service/langchain.js new file mode 100644 index 0000000000..af89096d20 --- /dev/null +++ b/designer/server/src/service/langchain.js @@ -0,0 +1,73 @@ +import Anthropic from '@anthropic-ai/sdk' +import { ChatAnthropic } from '@langchain/anthropic' +import { StringOutputParser } from '@langchain/core/output_parsers' +import { ChatPromptTemplate } from '@langchain/core/prompts' +import { RunnableSequence } from '@langchain/core/runnables' + +import config from '~/src/config.js' + +const MODEL = 'claude-3-5-haiku-20241022' + +const SYSTEM_TEMPLATE = `Answer the user as if you were an expert GDS copywriter. +Reply with a concise declarative phrase describing the form field heading they have entered. +If the user enters are declarative phrase or word, return the phrase or word verbatim. +Do not wrap the answer in quotations or apostrophes. +Do not end the answer in a full stop. +Questions should be reorganised into a declarative phrase, for example ‘What is your name?’ -> ‘Your name’. +For example 'Age' -> 'Age' +Declarative phrases should be left untouched, for example ‘Deadline date’ -> ‘Deadline date’. +If the user includes ‘your’ in the input, include ‘your’ in the output. +If the user doesn't include 'your' in the input, don't include 'your' in the output. +` +const promptTemplate = ChatPromptTemplate.fromMessages([ + ['system', SYSTEM_TEMPLATE], + ['user', '{text}'] +]) + +const model = new ChatAnthropic({ + apiKey: config.ai.anthropic.key, + model: MODEL, + maxTokens: config.ai.anthropic.maxTokens, + temperature: 0.2 +}) + +const chain = RunnableSequence.from([ + promptTemplate, + model, + new StringOutputParser() +]) + +/** + * @param {string} title + * @returns {Promise} + */ +export async function langchainJS(title) { + return await chain.invoke({ text: title }) +} + +const client = new Anthropic({ + apiKey: config.ai.anthropic.key +}) + +const cachedAnthropicCall = { + model: MODEL, + max_tokens: config.ai.anthropic.maxTokens, + system: [ + { + type: 'text', + text: SYSTEM_TEMPLATE, + cache_control: { type: 'ephemeral' } + } + ], + messages: [] +} + +export async function anthropicGetShortDescription(title) { + const response = await client.messages.create({ + ...cachedAnthropicCall, + messages: [{ role: 'user', content: title }] + }) + const [{ text }] = response.content + + return text +} diff --git a/model/src/form/form-editor/preview/question.js b/model/src/form/form-editor/preview/question.js index 83cb8aefe0..c3a561dcc3 100644 --- a/model/src/form/form-editor/preview/question.js +++ b/model/src/form/form-editor/preview/question.js @@ -111,6 +111,15 @@ export class Question extends PreviewComponent { this._hintText = value this.render() } + + set shortDescription(value) { + this._shortDescription = value + this.render() + } + + get shortDescription() { + return this._shortDescription + } } /** diff --git a/package-lock.json b/package-lock.json index eccb8f7349..07d4a7a23f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,12 @@ "model", "designer" ], + "dependencies": { + "@langchain/anthropic": "^0.3.23", + "@langchain/core": "^0.3.61", + "langchain": "^0.3.29", + "openai": "^5.8.2" + }, "devDependencies": { "@babel/cli": "^7.26.4", "@babel/core": "^7.26.10", @@ -284,6 +290,15 @@ "node": ">=6.0.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.52.0.tgz", + "integrity": "sha512-d4c+fg+xy9e46c8+YnrrgIQR45CZlAi7PwdzIfDXDM6ACxEZli1/fxhURsq30ZpMZy6LvSkr41jGq5aF5TD7rQ==", + "license": "MIT", + "bin": { + "anthropic-ai-sdk": "bin/cli" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.1.tgz", @@ -2095,6 +2110,12 @@ "integrity": "sha512-+2Mx67Y3skJ4NCD/qNSdBJNWtu6x6Qr53jeNg+QcwiL6mt0wK+3jwHH2x1p7xaYH6Ve2JKOVn0OxU35WsmqI9A==", "dev": true }, + "node_modules/@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "license": "MIT" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -4378,6 +4399,114 @@ "buffer": "^6.0.3" } }, + "node_modules/@langchain/anthropic": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-0.3.23.tgz", + "integrity": "sha512-lwp43HUcCM0bJqJEwBwutskvV85G3R3rQDW5XNCntPDzelW+fCmlsm40P7dg7uG/3uOtDGhj4eDMapKpbPvtlA==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "^0.52.0", + "fast-xml-parser": "^4.4.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.3.58 <0.4.0" + } + }, + "node_modules/@langchain/core": { + "version": "0.3.61", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.61.tgz", + "integrity": "sha512-4O7fw5SXNSE+uBnathLQrhm3t+7dZGagt/5kt37A+pXw0AkudxEBvveg73sSnpBd9SIz3/Vc7F4k8rCKXGbEDA==", + "license": "MIT", + "dependencies": { + "@cfworker/json-schema": "^4.0.2", + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": "^0.3.33", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^10.0.0", + "zod": "^3.25.32", + "zod-to-json-schema": "^3.22.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@langchain/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@langchain/core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/core/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/openai": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.5.16.tgz", + "integrity": "sha512-TqzPE3PM0bMkQi53qs8vCFkwaEp3VgwGw+s1e8Nas5ICCZZtc2XqcDPz4hf2gpo1k7/AZd6HuPlAsDy6wye9Qw==", + "license": "MIT", + "dependencies": { + "js-tiktoken": "^1.0.12", + "openai": "^5.3.0", + "zod": "^3.25.32" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.3.58 <0.4.0" + } + }, + "node_modules/@langchain/textsplitters": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@langchain/textsplitters/-/textsplitters-0.1.0.tgz", + "integrity": "sha512-djI4uw9rlkAb5iMhtLED+xJebDdAG935AdP4eRTB02R7OB/act55Bj9wsskhZsvuyQRpO4O1wQOp85s6T6GWmw==", + "license": "MIT", + "dependencies": { + "js-tiktoken": "^1.0.12" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.21 <0.4.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.10.tgz", @@ -4988,6 +5117,12 @@ "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", "license": "MIT" }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, "node_modules/@types/serialize-javascript": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/serialize-javascript/-/serialize-javascript-5.0.4.tgz", @@ -5020,6 +5155,12 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, "node_modules/@types/webpack-assets-manifest": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/@types/webpack-assets-manifest/-/webpack-assets-manifest-5.1.4.tgz", @@ -5979,8 +6120,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-query": { "version": "5.3.2", @@ -7383,6 +7523,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/console-table-printer": { + "version": "2.14.6", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.14.6.tgz", + "integrity": "sha512-MCBl5HNVaFuuHW6FGbL/4fB7N/ormCy+tQ+sxTrF6QtSbSNETvPuOVbkJBhzDgYhvjWGrTma4eYJa37ZuoQsPw==", + "license": "MIT", + "dependencies": { + "simple-wcswidth": "^1.0.1" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -7921,6 +8070,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", @@ -9743,6 +9901,24 @@ "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", "dev": true }, + "node_modules/fast-xml-parser": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.1.1" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -13025,6 +13201,15 @@ "node": ">=10" } }, + "node_modules/js-tiktoken": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.20.tgz", + "integrity": "sha512-Xlaqhhs8VfCd6Sh7a1cFkZHQbYTLCwVJJWiHVxBYzLPxW0XsoxBy1hitmjkdIjD3Aon5BXLHFwU5O8WUx6HH+A==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.5.1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -13034,7 +13219,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -13267,6 +13451,15 @@ "node": ">=6" } }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -13326,6 +13519,181 @@ "dev": true, "license": "MIT" }, + "node_modules/langchain": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.3.29.tgz", + "integrity": "sha512-L389pKlApVJPqu4hp58qY6NZAobI+MFPoBjSfjT1z3mcxtB68wLFGhaH4DVsTVg21NYO+0wTEoz24BWrxu9YGw==", + "license": "MIT", + "dependencies": { + "@langchain/openai": ">=0.1.0 <0.6.0", + "@langchain/textsplitters": ">=0.0.0 <0.2.0", + "js-tiktoken": "^1.0.12", + "js-yaml": "^4.1.0", + "jsonpointer": "^5.0.1", + "langsmith": "^0.3.33", + "openapi-types": "^12.1.3", + "p-retry": "4", + "uuid": "^10.0.0", + "yaml": "^2.2.1", + "zod": "^3.25.32" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/anthropic": "*", + "@langchain/aws": "*", + "@langchain/cerebras": "*", + "@langchain/cohere": "*", + "@langchain/core": ">=0.3.58 <0.4.0", + "@langchain/deepseek": "*", + "@langchain/google-genai": "*", + "@langchain/google-vertexai": "*", + "@langchain/google-vertexai-web": "*", + "@langchain/groq": "*", + "@langchain/mistralai": "*", + "@langchain/ollama": "*", + "@langchain/xai": "*", + "axios": "*", + "cheerio": "*", + "handlebars": "^4.7.8", + "peggy": "^3.0.2", + "typeorm": "*" + }, + "peerDependenciesMeta": { + "@langchain/anthropic": { + "optional": true + }, + "@langchain/aws": { + "optional": true + }, + "@langchain/cerebras": { + "optional": true + }, + "@langchain/cohere": { + "optional": true + }, + "@langchain/deepseek": { + "optional": true + }, + "@langchain/google-genai": { + "optional": true + }, + "@langchain/google-vertexai": { + "optional": true + }, + "@langchain/google-vertexai-web": { + "optional": true + }, + "@langchain/groq": { + "optional": true + }, + "@langchain/mistralai": { + "optional": true + }, + "@langchain/ollama": { + "optional": true + }, + "@langchain/xai": { + "optional": true + }, + "axios": { + "optional": true + }, + "cheerio": { + "optional": true + }, + "handlebars": { + "optional": true + }, + "peggy": { + "optional": true + }, + "typeorm": { + "optional": true + } + } + }, + "node_modules/langchain/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/langsmith": { + "version": "0.3.34", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.34.tgz", + "integrity": "sha512-rxYuuypqaSzIuNjZMTsCVAgG0cYdI516dwuKn58bu4YuBRlLaLeNlHewRyoqP9lrEAlpkekCV9fUwwZ7lO8f2g==", + "license": "MIT", + "dependencies": { + "@types/uuid": "^10.0.0", + "chalk": "^4.1.2", + "console-table-printer": "^2.12.1", + "p-queue": "^6.6.2", + "p-retry": "4", + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "openai": "*" + }, + "peerDependenciesMeta": { + "openai": { + "optional": true + } + } + }, + "node_modules/langsmith/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/langsmith/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/langsmith/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -13913,6 +14281,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/mylas": { "version": "2.1.13", "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz", @@ -14308,6 +14685,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.8.2.tgz", + "integrity": "sha512-8C+nzoHYgyYOXhHGN6r0fcb4SznuEn1R7YZMvlqDbnCuE0FM2mm3T1HiYW6WIcMS/F1Of2up/cSPjLPaWt0X9Q==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -14356,6 +14760,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -14395,6 +14808,53 @@ "node": ">=6" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -16180,6 +16640,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -17059,6 +17528,12 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/simple-wcswidth": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz", + "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==", + "license": "MIT" + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -17481,6 +17956,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/style-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", @@ -19252,7 +19739,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=10.0.0" }, @@ -19359,6 +19846,24 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } } } } diff --git a/package.json b/package.json index 6e2a55c00c..e255b4cc7e 100644 --- a/package.json +++ b/package.json @@ -72,5 +72,11 @@ "engines": { "node": "^22.11.0", "npm": "^10.9.0" + }, + "dependencies": { + "@langchain/anthropic": "^0.3.23", + "@langchain/core": "^0.3.61", + "langchain": "^0.3.29", + "openai": "^5.8.2" } } From 6b444b96d8d2e3125b6a5f7b91c3f316114ed58a Mon Sep 17 00:00:00 2001 From: Chris Cole Date: Mon, 30 Jun 2025 18:45:41 +0100 Subject: [PATCH 2/4] feat: 576858 - Improve system template --- designer/server/src/service/langchain.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/designer/server/src/service/langchain.js b/designer/server/src/service/langchain.js index af89096d20..907c67c78e 100644 --- a/designer/server/src/service/langchain.js +++ b/designer/server/src/service/langchain.js @@ -10,15 +10,17 @@ const MODEL = 'claude-3-5-haiku-20241022' const SYSTEM_TEMPLATE = `Answer the user as if you were an expert GDS copywriter. Reply with a concise declarative phrase describing the form field heading they have entered. -If the user enters are declarative phrase or word, return the phrase or word verbatim. +If the user enters a declarative phrase or word, return the phrase or word verbatim. Do not wrap the answer in quotations or apostrophes. Do not end the answer in a full stop. -Questions should be reorganised into a declarative phrase, for example ‘What is your name?’ -> ‘Your name’. -For example 'Age' -> 'Age' +Replace 'Do you...' with 'Whether you...'. +Questions should be reorganised into a declarative phrase, for example ‘What is your name?’ -> ‘Your name’, ‘Age’ -> ‘Age’. Declarative phrases should be left untouched, for example ‘Deadline date’ -> ‘Deadline date’. If the user includes ‘your’ in the input, include ‘your’ in the output. If the user doesn't include 'your' in the input, don't include 'your' in the output. +Remove any acronyms or brackets. ` + const promptTemplate = ChatPromptTemplate.fromMessages([ ['system', SYSTEM_TEMPLATE], ['user', '{text}'] From 0c97d3e69074d8c483960aae207c2d02cbf659db Mon Sep 17 00:00:00 2001 From: whitewater design Date: Fri, 11 Jul 2025 15:45:14 +0100 Subject: [PATCH 3/4] generate button --- .../src/javascripts/preview/question.js | 24 +++++++++++++------ .../forms/editor-v2/base-settings-fields.js | 3 ++- .../custom-templates/short-description.njk | 15 ++++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 designer/server/src/views/forms/editor-v2/custom-templates/short-description.njk diff --git a/designer/client/src/javascripts/preview/question.js b/designer/client/src/javascripts/preview/question.js index a927229f50..6291ad08c3 100644 --- a/designer/client/src/javascripts/preview/question.js +++ b/designer/client/src/javascripts/preview/question.js @@ -26,6 +26,9 @@ export class QuestionDomElements extends DomElements { const shortDescEl = /** @type {HTMLInputElement | null} */ ( document.getElementById('shortDescription') ) + const generateEl = /** @type {HTMLInputElement | null} */ ( + document.getElementById('generate') + ) /** * @type {HTMLInputElement|null} @@ -43,6 +46,7 @@ export class QuestionDomElements extends DomElements { * @type {HTMLInputElement|null} */ this.shortDesc = shortDescEl + this.generate = generateEl } /** @@ -162,18 +166,24 @@ export class EventListeners { ]) const shortDescriptionText = /** @type {ListenerRow} */ ([ - this.baseElements.question, + this.baseElements.generate, /** * @param {HTMLInputElement} target + * @param {Event} e */ - (target) => { - postShortDescription(target.value).then(({ shortDescription }) => { - this.baseElements.shortDesc.value = shortDescription - const event = new InputEvent('input', { bubbles: true }) - this.baseElements.shortDesc.dispatchEvent(event) + (target, e) => { + e.preventDefault() + postShortDescription(this.baseElements.question.value).then( + ({ shortDescription }) => { + this.baseElements.shortDesc.value = shortDescription + const event = new InputEvent('input', { bubbles: true }) + this.baseElements.shortDesc.dispatchEvent(event) + } + ).catch(e => { + console.error('Generate short description failed' e) }) }, - 'blur' + 'click' ]) const hintText = /** @type {ListenerRow} */ ([ diff --git a/designer/server/src/models/forms/editor-v2/base-settings-fields.js b/designer/server/src/models/forms/editor-v2/base-settings-fields.js index f9430d18c5..e26b60a080 100644 --- a/designer/server/src/models/forms/editor-v2/base-settings-fields.js +++ b/designer/server/src/models/forms/editor-v2/base-settings-fields.js @@ -164,7 +164,8 @@ export const allBaseSettingsFields = { }, hint: { text: "Enter a short description for this question like 'Licence period'. Short descriptions are used in error messages and on the check your answers page." - } + }, + customTemplate: 'short-description' }, fileTypes: { id: 'fileTypes', diff --git a/designer/server/src/views/forms/editor-v2/custom-templates/short-description.njk b/designer/server/src/views/forms/editor-v2/custom-templates/short-description.njk new file mode 100644 index 0000000000..ae66a704ce --- /dev/null +++ b/designer/server/src/views/forms/editor-v2/custom-templates/short-description.njk @@ -0,0 +1,15 @@ +
+ +
+ Enter a short description for this question like 'Licence period'. Short descriptions are used in error messages and on the check your answers page. +
+ + +
From 55634b2ff4d2b0bcd7ab46d3d977856dca7a49aa Mon Sep 17 00:00:00 2001 From: Mohammed Khalid Date: Tue, 10 Mar 2026 12:03:04 +0000 Subject: [PATCH 4/4] fix: Update question short description handling and model version --- .../client/src/javascripts/preview/question.js | 15 ++++++++++----- designer/server/src/service/langchain.js | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/designer/client/src/javascripts/preview/question.js b/designer/client/src/javascripts/preview/question.js index 6291ad08c3..2b6ceead0e 100644 --- a/designer/client/src/javascripts/preview/question.js +++ b/designer/client/src/javascripts/preview/question.js @@ -173,14 +173,19 @@ export class EventListeners { */ (target, e) => { e.preventDefault() - postShortDescription(this.baseElements.question.value).then( + const question = this.baseElements.question?.value ?? '' + postShortDescription(question).then( ({ shortDescription }) => { - this.baseElements.shortDesc.value = shortDescription - const event = new InputEvent('input', { bubbles: true }) - this.baseElements.shortDesc.dispatchEvent(event) + const shortDesc = this.baseElements.shortDesc + if (shortDesc) { + shortDesc.value = shortDescription + const event = new InputEvent('input', { bubbles: true }) + shortDesc.dispatchEvent(event) + } } ).catch(e => { - console.error('Generate short description failed' e) + // eslint-disable-next-line no-console -- POC + console.error('Generate short description failed', e) }) }, 'click' diff --git a/designer/server/src/service/langchain.js b/designer/server/src/service/langchain.js index 907c67c78e..79a5df6e7b 100644 --- a/designer/server/src/service/langchain.js +++ b/designer/server/src/service/langchain.js @@ -6,7 +6,7 @@ import { RunnableSequence } from '@langchain/core/runnables' import config from '~/src/config.js' -const MODEL = 'claude-3-5-haiku-20241022' +const MODEL = 'claude-haiku-4-5-20251001' const SYSTEM_TEMPLATE = `Answer the user as if you were an expert GDS copywriter. Reply with a concise declarative phrase describing the form field heading they have entered.