Update ai validation to use gpt-5 and the new responses api

This commit is contained in:
2025-10-01 22:18:26 -04:00
parent e10df632d8
commit 60875c25a6
7 changed files with 1184 additions and 494 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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;
}