Update ai validation to use gpt-5 and the new responses api
This commit is contained in:
133
inventory-server/package-lock.json
generated
133
inventory-server/package-lock.json
generated
@@ -20,7 +20,7 @@
|
||||
"express": "^4.18.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.12.0",
|
||||
"openai": "^4.85.3",
|
||||
"openai": "^6.0.0",
|
||||
"pg": "^8.14.1",
|
||||
"pm2": "^5.3.0",
|
||||
"ssh2": "^1.16.0",
|
||||
@@ -399,43 +399,12 @@
|
||||
"integrity": "sha512-R/BHQFripuhW6XPXy05hIvXJQdQ4540KnTvEFHSLjXfHYM41liOLKgIJEyYYiQe796xpaMHfe4Uj/p7Uvng2vA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.19.76",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.76.tgz",
|
||||
"integrity": "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
|
||||
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -458,18 +427,6 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/agentkeepalive": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
|
||||
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"humanize-ms": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/amp": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz",
|
||||
@@ -1327,15 +1284,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter2": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz",
|
||||
@@ -1474,25 +1422,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data-encoder": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
|
||||
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/formdata-node": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
|
||||
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "1.0.0",
|
||||
"web-streams-polyfill": "4.0.0-beta.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -1853,15 +1782,6 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/humanize-ms": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
||||
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
@@ -2407,25 +2327,6 @@
|
||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
@@ -2622,25 +2523,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "4.85.3",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.85.3.tgz",
|
||||
"integrity": "sha512-KTMXAK6FPd2IvsPtglMt0J1GyVrjMxCYzu/mVbCPabzzquSJoZlYpHtE0p0ScZPyt11XTc757xSO4j39j5g+Xw==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-6.0.0.tgz",
|
||||
"integrity": "sha512-J7LEmTn3WLZnbyEmMYcMPyT5A0fGzhPwSvVUcNRKy6j2hJIbqSFrJERnUHYNkcoCCalRumypnj9AVoe5bVHd3Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.25 || ^4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
@@ -3941,12 +3833,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
@@ -4017,15 +3903,6 @@
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "4.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"express": "^4.18.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.12.0",
|
||||
"openai": "^4.85.3",
|
||||
"openai": "^6.0.0",
|
||||
"pg": "^8.14.1",
|
||||
"pm2": "^5.3.0",
|
||||
"ssh2": "^1.16.0",
|
||||
|
||||
@@ -6,7 +6,7 @@ const path = require("path");
|
||||
const dotenv = require("dotenv");
|
||||
const mysql = require('mysql2/promise');
|
||||
const { Client } = require('ssh2');
|
||||
const { getDbConnection } = require('../utils/dbConnection'); // Import the optimized connection function
|
||||
const { getDbConnection, closeAllConnections } = require('../utils/dbConnection'); // Import the optimized connection function
|
||||
|
||||
// Ensure environment variables are loaded
|
||||
dotenv.config({ path: path.join(__dirname, "../../.env") });
|
||||
@@ -19,6 +19,121 @@ if (!process.env.OPENAI_API_KEY) {
|
||||
console.error("Warning: OPENAI_API_KEY is not set in environment variables");
|
||||
}
|
||||
|
||||
async function createResponsesCompletion(payload) {
|
||||
if (!openai.responses?.create) {
|
||||
throw new Error(
|
||||
"OpenAI client does not expose responses.create; please verify the openai SDK version."
|
||||
);
|
||||
}
|
||||
|
||||
return openai.responses.create(payload);
|
||||
}
|
||||
|
||||
const AI_VALIDATION_SCHEMA_NAME = "ai_validation_response";
|
||||
|
||||
const FLEXIBLE_PRIMITIVE_SCHEMAS = [
|
||||
{ type: "string" },
|
||||
{ type: "number" },
|
||||
{ type: "boolean" },
|
||||
{ type: "null" },
|
||||
];
|
||||
|
||||
const FLEXIBLE_ARRAY_SCHEMA = {
|
||||
type: "array",
|
||||
items: {
|
||||
anyOf: FLEXIBLE_PRIMITIVE_SCHEMAS,
|
||||
},
|
||||
};
|
||||
|
||||
const FLEXIBLE_OBJECT_SCHEMA = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
patternProperties: {
|
||||
".+": {
|
||||
anyOf: [...FLEXIBLE_PRIMITIVE_SCHEMAS, FLEXIBLE_ARRAY_SCHEMA],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const FLEXIBLE_VALUE_SCHEMA = {
|
||||
anyOf: [...FLEXIBLE_PRIMITIVE_SCHEMAS, FLEXIBLE_ARRAY_SCHEMA, FLEXIBLE_OBJECT_SCHEMA],
|
||||
};
|
||||
|
||||
const AI_VALIDATION_JSON_SCHEMA = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: [
|
||||
"correctedData",
|
||||
"changes",
|
||||
"warnings",
|
||||
"summary",
|
||||
"qualityNotes",
|
||||
"nextSteps",
|
||||
"metadata"
|
||||
],
|
||||
properties: {
|
||||
correctedData: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
patternProperties: {
|
||||
".+": FLEXIBLE_VALUE_SCHEMA,
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
changes: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
default: [],
|
||||
},
|
||||
warnings: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
default: [],
|
||||
},
|
||||
summary: {
|
||||
type: "string",
|
||||
default: "",
|
||||
},
|
||||
qualityNotes: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
default: [],
|
||||
},
|
||||
nextSteps: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
default: [],
|
||||
},
|
||||
metadata: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
patternProperties: {
|
||||
".+": FLEXIBLE_VALUE_SCHEMA,
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const AI_VALIDATION_TEXT_FORMAT = {
|
||||
type: "json_schema",
|
||||
name: AI_VALIDATION_SCHEMA_NAME,
|
||||
strict: true,
|
||||
schema: AI_VALIDATION_JSON_SCHEMA,
|
||||
};
|
||||
|
||||
// Debug endpoint for viewing prompt
|
||||
router.post("/debug", async (req, res) => {
|
||||
try {
|
||||
@@ -139,6 +254,12 @@ router.post("/debug", async (req, res) => {
|
||||
code: error.code || null,
|
||||
name: error.name || null
|
||||
});
|
||||
} finally {
|
||||
try {
|
||||
await closeAllConnections();
|
||||
} catch (closeError) {
|
||||
console.error("⚠️ Failed to close DB connections after debug request:", closeError);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -402,8 +523,9 @@ async function generateDebugResponse(productsToUse, res) {
|
||||
|
||||
console.log("Sending response with taxonomy stats:", response.taxonomyStats);
|
||||
return response;
|
||||
} finally {
|
||||
if (promptConnection) await promptConnection.end();
|
||||
} catch (promptLoadError) {
|
||||
console.error("Error loading prompt:", promptLoadError);
|
||||
throw promptLoadError;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error generating debug response:", error);
|
||||
@@ -883,34 +1005,80 @@ router.post("/validate", async (req, res) => {
|
||||
console.log("🔄 Loading prompt with filtered taxonomy...");
|
||||
const promptData = await loadPrompt(connection, products, req.app.locals.pool);
|
||||
const fullUserPrompt = promptData.userContent + "\n" + JSON.stringify(products);
|
||||
const promptLength = promptData.systemInstructions.length + fullUserPrompt.length; // Store prompt length for performance metrics
|
||||
promptLength = promptData.systemInstructions.length + fullUserPrompt.length; // Store prompt length for performance metrics
|
||||
console.log("📝 Generated prompt length:", promptLength);
|
||||
console.log("📝 System instructions length:", promptData.systemInstructions.length);
|
||||
console.log("📝 User content length:", fullUserPrompt.length);
|
||||
|
||||
console.log("🤖 Sending request to OpenAI...");
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
console.log("🤖 Sending request to OpenAI Responses API...");
|
||||
|
||||
// GPT-5 Responses API Configuration:
|
||||
// - Using "gpt-5" (reasoning model) for complex product validation
|
||||
// - reasoning.effort: "medium" balances quality and speed (minimal, low, medium, high)
|
||||
// - text.verbosity: "medium" provides balanced output detail (low, medium, high)
|
||||
// - max_output_tokens: 20000 ensures space for large product batches
|
||||
// Note: Responses API is the recommended endpoint for GPT-5 models
|
||||
const completion = await createResponsesCompletion({
|
||||
model: "gpt-5",
|
||||
input: [
|
||||
{
|
||||
role: "system",
|
||||
content: promptData.systemInstructions,
|
||||
role: "developer",
|
||||
content: `${promptData.systemInstructions}\n\nYou MUST respond with a single valid JSON object containing the following top-level keys: correctedData, changes, warnings, summary, qualityNotes, nextSteps, metadata.\n- correctedData: array of product objects reflecting the updated data.\n- changes: array of human-readable bullet points summarizing the nature of updates.\n- warnings: array of caveats or risks that still require review.\n- summary: a concise paragraph (<=75 words) describing overall data quality and improvements.\n- qualityNotes: array of short comments (<=40 words each) about validation quality or notable observations.\n- nextSteps: array of recommended manual follow-up actions (if none, provide an empty array).\n- metadata: object containing any supplemental machine-readable information (optional fields allowed).\nDo NOT include Markdown code fences or any text outside the JSON object.`,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: fullUserPrompt,
|
||||
},
|
||||
],
|
||||
temperature: 0.2,
|
||||
response_format: { type: "json_object" },
|
||||
reasoning: {
|
||||
effort: "medium"
|
||||
},
|
||||
text: {
|
||||
verbosity: "medium",
|
||||
format: AI_VALIDATION_TEXT_FORMAT,
|
||||
},
|
||||
max_output_tokens: 20000,
|
||||
});
|
||||
|
||||
console.log("✅ Received response from OpenAI");
|
||||
const rawResponse = completion.choices[0].message.content;
|
||||
console.log("📄 Raw AI response length:", rawResponse.length);
|
||||
console.log("✅ Received response from OpenAI Responses API");
|
||||
|
||||
// Responses API structure: response has 'output' array with message objects
|
||||
const rawResponse = extractResponseText(completion);
|
||||
console.log("📄 Raw AI response length:", rawResponse ? rawResponse.length : 0);
|
||||
|
||||
if (!rawResponse) {
|
||||
throw new Error("OpenAI response did not include any text output");
|
||||
}
|
||||
|
||||
const responseModel = completion.model;
|
||||
const usage = completion.usage || {};
|
||||
|
||||
// GPT-5 Responses API provides detailed token usage including reasoning tokens
|
||||
const tokenUsageSummary = {
|
||||
prompt: usage.input_tokens ?? usage.prompt_tokens ?? null,
|
||||
completion: usage.output_tokens ?? usage.completion_tokens ?? null,
|
||||
total: usage.total_tokens ?? null,
|
||||
// GPT-5 reasoning tokens are in output_tokens_details
|
||||
reasoning: usage.output_tokens_details?.reasoning_tokens ?? usage.completion_tokens_details?.reasoning_tokens ?? null,
|
||||
// Also capture text generation tokens separately from reasoning
|
||||
textGeneration: usage.output_tokens_details?.text_generation_tokens ?? usage.completion_tokens_details?.text_generation_tokens ?? null,
|
||||
cachedPrompt: usage.input_tokens_details?.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? null,
|
||||
// Capture audio tokens if present (future GPT-5 feature)
|
||||
audioTokens: usage.output_tokens_details?.audio_tokens ?? usage.completion_tokens_details?.audio_tokens ?? null,
|
||||
};
|
||||
|
||||
// Extract reasoning_effort and verbosity that were actually applied
|
||||
const reasoningEffortApplied = completion.reasoning?.effort || "medium";
|
||||
const verbosityApplied = completion.text?.verbosity || "medium";
|
||||
|
||||
console.log("📊 Token usage summary:", tokenUsageSummary);
|
||||
console.log("🤖 Model dispatched:", responseModel);
|
||||
console.log("🧠 Reasoning effort applied:", reasoningEffortApplied);
|
||||
console.log("📝 Verbosity applied:", verbosityApplied);
|
||||
|
||||
try {
|
||||
const aiResponse = JSON.parse(rawResponse);
|
||||
const normalizedResponse = normalizeJsonResponse(rawResponse);
|
||||
const aiResponse = JSON.parse(normalizedResponse);
|
||||
console.log(
|
||||
"🔄 Parsed AI response with keys:",
|
||||
Object.keys(aiResponse)
|
||||
@@ -975,7 +1143,12 @@ router.post("/validate", async (req, res) => {
|
||||
const endTime = new Date();
|
||||
let performanceMetrics = {
|
||||
promptLength,
|
||||
productCount: products.length
|
||||
productCount: products.length,
|
||||
model: responseModel,
|
||||
tokenUsage: tokenUsageSummary,
|
||||
reasoningTokens: tokenUsageSummary.reasoning,
|
||||
reasoningEffort: reasoningEffortApplied,
|
||||
verbosity: verbosityApplied,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -1040,83 +1213,78 @@ router.post("/validate", async (req, res) => {
|
||||
let promptSources = null;
|
||||
|
||||
try {
|
||||
// Get system prompt
|
||||
const systemPromptResult = await pool.query(`
|
||||
SELECT * FROM ai_prompts WHERE prompt_type = 'system'
|
||||
`);
|
||||
|
||||
// Get general prompt
|
||||
const generalPromptResult = await pool.query(`
|
||||
SELECT * FROM ai_prompts WHERE prompt_type = 'general'
|
||||
`);
|
||||
|
||||
// Extract unique company IDs from products
|
||||
const companyIds = new Set();
|
||||
products.forEach(product => {
|
||||
if (product.company) {
|
||||
companyIds.add(String(product.company));
|
||||
}
|
||||
});
|
||||
|
||||
let companyPrompts = [];
|
||||
if (companyIds.size > 0) {
|
||||
// Fetch company-specific prompts
|
||||
const companyPromptsResult = await pool.query(`
|
||||
SELECT * FROM ai_prompts
|
||||
WHERE prompt_type = 'company_specific'
|
||||
AND company = ANY($1)
|
||||
`, [Array.from(companyIds)]);
|
||||
|
||||
companyPrompts = companyPromptsResult.rows;
|
||||
}
|
||||
|
||||
// Find company names from taxonomy for the validation endpoint
|
||||
const companyPromptsWithNames = companyPrompts.map(prompt => {
|
||||
let companyName = "Unknown Company";
|
||||
if (taxonomy.companies && Array.isArray(taxonomy.companies)) {
|
||||
const companyData = taxonomy.companies.find(company =>
|
||||
String(company[0]) === String(prompt.company)
|
||||
);
|
||||
if (companyData && companyData[1]) {
|
||||
companyName = companyData[1];
|
||||
// Use the local PostgreSQL pool from the app
|
||||
const pool = req.app.locals.pool;
|
||||
if (!pool) {
|
||||
console.warn("⚠️ Local database pool not available for prompt sources");
|
||||
} else {
|
||||
// Get system prompt
|
||||
const systemPromptResult = await pool.query(`
|
||||
SELECT * FROM ai_prompts WHERE prompt_type = 'system'
|
||||
`);
|
||||
|
||||
// Get general prompt
|
||||
const generalPromptResult = await pool.query(`
|
||||
SELECT * FROM ai_prompts WHERE prompt_type = 'general'
|
||||
`);
|
||||
|
||||
// Extract unique company IDs from products
|
||||
const companyIds = new Set();
|
||||
products.forEach(product => {
|
||||
if (product.company) {
|
||||
companyIds.add(String(product.company));
|
||||
}
|
||||
});
|
||||
|
||||
let companyPrompts = [];
|
||||
if (companyIds.size > 0) {
|
||||
// Fetch company-specific prompts
|
||||
const companyPromptsResult = await pool.query(`
|
||||
SELECT * FROM ai_prompts
|
||||
WHERE prompt_type = 'company_specific'
|
||||
AND company = ANY($1)
|
||||
`, [Array.from(companyIds)]);
|
||||
|
||||
companyPrompts = companyPromptsResult.rows;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
// Format company prompts for response
|
||||
// Note: Company names would require re-fetching taxonomy data
|
||||
// For now, we include company ID only
|
||||
const companyPromptsWithNames = companyPrompts.map(prompt => ({
|
||||
id: prompt.id,
|
||||
company: prompt.company,
|
||||
companyName: companyName,
|
||||
prompt_text: prompt.prompt_text
|
||||
};
|
||||
});
|
||||
|
||||
// Set prompt sources
|
||||
if (generalPromptResult.rows.length > 0) {
|
||||
const generalPrompt = generalPromptResult.rows[0];
|
||||
let systemPrompt = null;
|
||||
|
||||
if (systemPromptResult.rows.length > 0) {
|
||||
systemPrompt = systemPromptResult.rows[0];
|
||||
}));
|
||||
|
||||
// Set prompt sources
|
||||
if (generalPromptResult.rows.length > 0) {
|
||||
const generalPrompt = generalPromptResult.rows[0];
|
||||
let systemPrompt = null;
|
||||
|
||||
if (systemPromptResult.rows.length > 0) {
|
||||
systemPrompt = systemPromptResult.rows[0];
|
||||
}
|
||||
|
||||
promptSources = {
|
||||
...(systemPrompt ? {
|
||||
systemPrompt: {
|
||||
id: systemPrompt.id,
|
||||
prompt_text: systemPrompt.prompt_text
|
||||
}
|
||||
} : {
|
||||
systemPrompt: {
|
||||
id: 0,
|
||||
prompt_text: `You are a specialized e-commerce product data processor for a crafting supplies website tasked with providing complete, correct, appealing, and SEO-friendly product listings. You should write professionally, but in a friendly and engaging tone. You have meticulous attention to detail and are a master at your craft.`
|
||||
}
|
||||
}),
|
||||
generalPrompt: {
|
||||
id: generalPrompt.id,
|
||||
prompt_text: generalPrompt.prompt_text
|
||||
},
|
||||
companyPrompts: companyPromptsWithNames
|
||||
};
|
||||
}
|
||||
|
||||
promptSources = {
|
||||
...(systemPrompt ? {
|
||||
systemPrompt: {
|
||||
id: systemPrompt.id,
|
||||
prompt_text: systemPrompt.prompt_text
|
||||
}
|
||||
} : {
|
||||
systemPrompt: {
|
||||
id: 0,
|
||||
prompt_text: `You are a specialized e-commerce product data processor for a crafting supplies website tasked with providing complete, correct, appealing, and SEO-friendly product listings. You should write professionally, but in a friendly and engaging tone. You have meticulous attention to detail and are a master at your craft.`
|
||||
}
|
||||
}),
|
||||
generalPrompt: {
|
||||
id: generalPrompt.id,
|
||||
prompt_text: generalPrompt.prompt_text
|
||||
},
|
||||
companyPrompts: companyPromptsWithNames
|
||||
};
|
||||
}
|
||||
} catch (promptSourceError) {
|
||||
console.error("⚠️ Error getting prompt sources:", promptSourceError);
|
||||
@@ -1126,16 +1294,26 @@ router.post("/validate", async (req, res) => {
|
||||
// Include prompt sources in the response
|
||||
res.json({
|
||||
success: true,
|
||||
changeDetails: changeDetails,
|
||||
performanceMetrics: performanceMetrics || {
|
||||
// Fallback: calculate a simple estimate
|
||||
promptLength: promptLength,
|
||||
processingTimeSeconds: Math.max(15, Math.round(promptLength / 1000)),
|
||||
isEstimate: true,
|
||||
productCount: products.length
|
||||
},
|
||||
promptSources: promptSources,
|
||||
...aiResponse,
|
||||
changeDetails,
|
||||
performanceMetrics:
|
||||
performanceMetrics || {
|
||||
// Fallback: calculate a simple estimate
|
||||
promptLength,
|
||||
processingTimeSeconds: Math.max(15, Math.round(promptLength / 1000)),
|
||||
isEstimate: true,
|
||||
productCount: products.length,
|
||||
model: responseModel,
|
||||
tokenUsage: tokenUsageSummary,
|
||||
reasoningTokens: tokenUsageSummary.reasoning,
|
||||
reasoningEffort: reasoningEffortApplied,
|
||||
verbosity: verbosityApplied,
|
||||
},
|
||||
promptSources,
|
||||
model: responseModel,
|
||||
tokenUsage: tokenUsageSummary,
|
||||
reasoningEffort: reasoningEffortApplied,
|
||||
verbosity: verbosityApplied,
|
||||
});
|
||||
} catch (parseError) {
|
||||
console.error("❌ Error parsing AI response:", parseError);
|
||||
@@ -1151,10 +1329,6 @@ router.post("/validate", async (req, res) => {
|
||||
success: false,
|
||||
error: "OpenAI API Error: " + openaiError.message,
|
||||
});
|
||||
} finally {
|
||||
// Clean up database connection and SSH tunnel
|
||||
if (connection) await connection.end();
|
||||
if (ssh) ssh.end();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ AI Validation Error:", error);
|
||||
@@ -1167,6 +1341,12 @@ router.post("/validate", async (req, res) => {
|
||||
success: false,
|
||||
error: error.message || "Error during AI validation",
|
||||
});
|
||||
} finally {
|
||||
try {
|
||||
await closeAllConnections();
|
||||
} catch (closeError) {
|
||||
console.error("⚠️ Failed to close DB connections after validation request:", closeError);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1249,8 +1429,11 @@ router.get("/test-taxonomy", async (req, res) => {
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} finally {
|
||||
if (connection) await connection.end();
|
||||
if (ssh) ssh.end();
|
||||
try {
|
||||
await closeAllConnections();
|
||||
} catch (closeError) {
|
||||
console.error("⚠️ Failed to close DB connections after test-taxonomy request:", closeError);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Test taxonomy endpoint error:", error);
|
||||
@@ -1262,3 +1445,99 @@ router.get("/test-taxonomy", async (req, res) => {
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
function extractResponseText(response) {
|
||||
if (!response) return "";
|
||||
|
||||
const outputs = [];
|
||||
if (Array.isArray(response.output)) {
|
||||
outputs.push(...response.output);
|
||||
}
|
||||
if (Array.isArray(response.outputs)) {
|
||||
outputs.push(...response.outputs);
|
||||
}
|
||||
|
||||
const segments = outputs.flatMap((output) => collectTextSegments(output?.content ?? output));
|
||||
|
||||
if (segments.length === 0 && typeof response.output_text === "string") {
|
||||
segments.push(response.output_text);
|
||||
}
|
||||
|
||||
if (segments.length === 0 && response.choices?.length) {
|
||||
segments.push(
|
||||
...collectTextSegments(response.choices?.[0]?.message?.content)
|
||||
);
|
||||
}
|
||||
|
||||
const text = segments.join("").trim();
|
||||
return text;
|
||||
}
|
||||
|
||||
function collectTextSegments(node) {
|
||||
if (node == null) return [];
|
||||
|
||||
if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") {
|
||||
return [String(node)];
|
||||
}
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
return node.flatMap(collectTextSegments);
|
||||
}
|
||||
|
||||
if (typeof node !== "object") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const segments = [];
|
||||
|
||||
if (typeof node.text === "string") {
|
||||
segments.push(node.text);
|
||||
} else if (Array.isArray(node.text)) {
|
||||
segments.push(...node.text.flatMap(collectTextSegments));
|
||||
}
|
||||
|
||||
if (typeof node.content === "string") {
|
||||
segments.push(node.content);
|
||||
} else if (Array.isArray(node.content)) {
|
||||
segments.push(...node.content.flatMap(collectTextSegments));
|
||||
}
|
||||
|
||||
if (typeof node.output_text === "string") {
|
||||
segments.push(node.output_text);
|
||||
} else if (Array.isArray(node.output_text)) {
|
||||
segments.push(...node.output_text.flatMap(collectTextSegments));
|
||||
}
|
||||
|
||||
if (typeof node.value === "string") {
|
||||
segments.push(node.value);
|
||||
}
|
||||
|
||||
if (typeof node.data === "string") {
|
||||
segments.push(node.data);
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
function normalizeJsonResponse(text) {
|
||||
if (!text || typeof text !== 'string') return text;
|
||||
let cleaned = text.trim();
|
||||
|
||||
if (cleaned.startsWith('```')) {
|
||||
const firstLineBreak = cleaned.indexOf('\n');
|
||||
if (firstLineBreak !== -1) {
|
||||
cleaned = cleaned.substring(firstLineBreak + 1);
|
||||
} else {
|
||||
cleaned = cleaned.replace(/^```/, '');
|
||||
}
|
||||
|
||||
const closingFenceIndex = cleaned.lastIndexOf('```');
|
||||
if (closingFenceIndex !== -1) {
|
||||
cleaned = cleaned.substring(0, closingFenceIndex);
|
||||
}
|
||||
|
||||
cleaned = cleaned.trim();
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user