351 lines
10 KiB
JavaScript
351 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
// Demo script that actually uses the PoC functionality to demonstrate real query generation
|
||
import fs from "node:fs";
|
||
import OpenAI from "openai";
|
||
|
||
// Import PoC components (we'll need to extract them to make them reusable)
|
||
const MODEL = process.env.OPENAI_MODEL || "gpt-5";
|
||
const ODMDB_BASE_PATH = "../smatchitObjectOdmdb";
|
||
const SCHEMA_PATH = `${ODMDB_BASE_PATH}/schema`;
|
||
|
||
console.log("🚀 ODMDB NL to Query Demo - Live PoC Testing");
|
||
console.log("=".repeat(60));
|
||
|
||
// Check prerequisites
|
||
if (!process.env.OPENAI_API_KEY) {
|
||
console.log("❌ Missing OPENAI_API_KEY environment variable");
|
||
console.log(" Set it with: export OPENAI_API_KEY=sk-your-api-key");
|
||
process.exit(1);
|
||
}
|
||
|
||
// Load schema (same function as in poc.js)
|
||
function loadJsonSafe(path) {
|
||
try {
|
||
if (fs.existsSync(path)) {
|
||
return JSON.parse(fs.readFileSync(path, "utf-8"));
|
||
}
|
||
} catch (e) {
|
||
console.warn(`Warning: Could not load ${path}:`, e.message);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Load actual ODMDB schemas
|
||
const SCHEMAS = {
|
||
seekers: loadJsonSafe(`${SCHEMA_PATH}/seekers.json`),
|
||
main: loadJsonSafe("./main.json"), // Fallback consolidated schema
|
||
};
|
||
|
||
// Simplified SchemaMapper for demo
|
||
class DemoSchemaMapper {
|
||
constructor(schemas) {
|
||
this.seekersSchema = schemas.seekers;
|
||
console.log(
|
||
`📋 Loaded seekers schema with ${
|
||
Object.keys(this.seekersSchema?.properties || {}).length
|
||
} properties`
|
||
);
|
||
}
|
||
|
||
getRecruiterReadableFields() {
|
||
if (!this.seekersSchema?.apxaccessrights?.recruiters?.R) {
|
||
return ["alias", "email", "seekstatus", "seekworkingyear"];
|
||
}
|
||
return this.seekersSchema.apxaccessrights.recruiters.R;
|
||
}
|
||
|
||
getAllSeekersFields() {
|
||
if (!this.seekersSchema?.properties) return [];
|
||
return Object.keys(this.seekersSchema.properties);
|
||
}
|
||
}
|
||
|
||
const schemaMapper = new DemoSchemaMapper(SCHEMAS);
|
||
|
||
// Sample queries to demonstrate with actual PoC execution
|
||
const demoQueries = [
|
||
{
|
||
nl: "show me seekers with status startasap and their email and experience",
|
||
description: "Status-based filtering with field selection",
|
||
},
|
||
{
|
||
nl: "find seekers looking for jobs urgently with salary expectations",
|
||
description: "Status synonym mapping + salary field",
|
||
},
|
||
{
|
||
nl: "get seekers with their contact info and personality types",
|
||
description: "Multiple field types (contact + MBTI)",
|
||
},
|
||
];
|
||
|
||
console.log("<22> Demo Queries - Testing Live PoC:");
|
||
|
||
// JSON Schema for query generation (same as poc.js)
|
||
function buildResponseJsonSchema() {
|
||
const recruiterReadableFields = schemaMapper.getRecruiterReadableFields();
|
||
return {
|
||
type: "object",
|
||
additionalProperties: false,
|
||
properties: {
|
||
object: { type: "string", enum: ["seekers"] },
|
||
condition: { type: "array", items: { type: "string" }, minItems: 1 },
|
||
fields: {
|
||
type: "array",
|
||
items: { type: "string", enum: recruiterReadableFields },
|
||
minItems: 1,
|
||
},
|
||
},
|
||
required: ["object", "condition", "fields"],
|
||
};
|
||
}
|
||
|
||
// System prompt (simplified version from poc.js)
|
||
function systemPrompt() {
|
||
const availableFields = schemaMapper.getAllSeekersFields();
|
||
const recruiterReadableFields = schemaMapper.getRecruiterReadableFields();
|
||
|
||
return [
|
||
"You convert a natural language request into an ODMDB search payload.",
|
||
"Return ONLY a compact JSON object that matches the provided JSON Schema.",
|
||
"",
|
||
"ODMDB DSL:",
|
||
"- idx.<indexName>(value) - for indexed fields",
|
||
"- prop.<field>(operator:value) - for direct property queries",
|
||
"",
|
||
"Available seekers fields:",
|
||
availableFields.slice(0, 15).join(", ") +
|
||
(availableFields.length > 15 ? "..." : ""),
|
||
"",
|
||
"Recruiter-readable fields (use these for field selection):",
|
||
recruiterReadableFields.join(", "),
|
||
"",
|
||
"Field mappings:",
|
||
"- 'email', 'contact info' → email",
|
||
"- 'experience', 'years of experience' → seekworkingyear",
|
||
"- 'status', 'availability' → seekstatus",
|
||
"- 'salary', 'pay' → salaryexpectation",
|
||
"- 'personality', 'MBTI' → mbti",
|
||
"",
|
||
"Status value mappings:",
|
||
"- 'urgent', 'urgently', 'ASAP' → startasap",
|
||
"- 'no rush', 'taking time' → norush",
|
||
"- 'not looking' → notlooking",
|
||
"",
|
||
"Rules: Object must be 'seekers'. Use idx.seekstatus_alias for status queries.",
|
||
].join("\n");
|
||
}
|
||
|
||
// OpenAI client and query function
|
||
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||
|
||
async function generateQuery(nlText) {
|
||
try {
|
||
const resp = await client.responses.create({
|
||
model: MODEL,
|
||
input: [
|
||
{ role: "system", content: systemPrompt() },
|
||
{
|
||
role: "user",
|
||
content: `Natural language request: "${nlText}"\nReturn ONLY the JSON object.`,
|
||
},
|
||
],
|
||
text: {
|
||
format: {
|
||
name: "OdmdbQuery",
|
||
type: "json_schema",
|
||
schema: buildResponseJsonSchema(),
|
||
strict: true,
|
||
},
|
||
},
|
||
});
|
||
|
||
const jsonText = resp.output_text || resp.output?.[0]?.content?.[0]?.text;
|
||
return JSON.parse(jsonText);
|
||
} catch (error) {
|
||
console.error(`❌ Query generation failed: ${error.message}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Simple query execution (simplified from poc.js)
|
||
function loadSeekersData() {
|
||
const seekersItemsPath = `${ODMDB_BASE_PATH}/objects/seekers/itm`;
|
||
try {
|
||
const files = fs
|
||
.readdirSync(seekersItemsPath)
|
||
.filter((file) => file.endsWith(".json") && file !== "backup")
|
||
.slice(0, 10); // Just 10 files for demo speed
|
||
|
||
const seekers = [];
|
||
for (const file of files) {
|
||
try {
|
||
const filePath = `${seekersItemsPath}/${file}`;
|
||
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||
seekers.push(data);
|
||
} catch (error) {
|
||
// Skip invalid files
|
||
}
|
||
}
|
||
return seekers;
|
||
} catch (error) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
async function executeQuery(query) {
|
||
const allSeekers = loadSeekersData();
|
||
if (allSeekers.length === 0) return { data: [] };
|
||
|
||
let filteredSeekers = allSeekers;
|
||
|
||
// Simple filtering
|
||
for (const condition of query.condition) {
|
||
if (condition.includes("idx.seekstatus_alias(startasap)")) {
|
||
filteredSeekers = filteredSeekers.filter(
|
||
(seeker) => seeker.seekstatus === "startasap"
|
||
);
|
||
}
|
||
if (condition.includes("prop.salaryexpectation(exists:true)")) {
|
||
filteredSeekers = filteredSeekers.filter(
|
||
(seeker) => seeker.salaryexpectation
|
||
);
|
||
}
|
||
if (condition.includes("prop.email(exists:true)")) {
|
||
filteredSeekers = filteredSeekers.filter((seeker) => seeker.email);
|
||
}
|
||
if (condition.includes("prop.mbti(exists:true)")) {
|
||
filteredSeekers = filteredSeekers.filter((seeker) => seeker.mbti);
|
||
}
|
||
}
|
||
|
||
// Select only requested fields
|
||
const results = filteredSeekers.map((seeker) => {
|
||
const filtered = {};
|
||
for (const field of query.fields) {
|
||
if (seeker.hasOwnProperty(field)) {
|
||
filtered[field] = seeker[field];
|
||
}
|
||
}
|
||
return filtered;
|
||
});
|
||
|
||
return { data: results };
|
||
}
|
||
|
||
// Main demo execution
|
||
async function runDemo() {
|
||
const executeQueries = process.env.EXECUTE_DEMO === "true";
|
||
|
||
for (let i = 0; i < demoQueries.length; i++) {
|
||
const query = demoQueries[i];
|
||
console.log(`\n${i + 1}. "${query.nl}"`);
|
||
console.log(` Purpose: ${query.description}`);
|
||
|
||
console.log(" 🤖 Generating query...");
|
||
const generatedQuery = await generateQuery(query.nl);
|
||
|
||
if (generatedQuery) {
|
||
console.log(" ✅ Generated ODMDB Query:");
|
||
console.log(
|
||
` ${JSON.stringify(generatedQuery, null, 6).replace(/\n/g, "\n ")}`
|
||
);
|
||
|
||
if (executeQueries) {
|
||
console.log(" 🔍 Executing query...");
|
||
const results = await executeQuery(generatedQuery);
|
||
console.log(` 📊 Found ${results.data.length} results`);
|
||
|
||
if (results.data.length > 0) {
|
||
console.log(" 📋 Sample result:");
|
||
console.log(
|
||
` ${JSON.stringify(results.data[0], null, 6).replace(
|
||
/\n/g,
|
||
"\n "
|
||
)}`
|
||
);
|
||
}
|
||
}
|
||
} else {
|
||
console.log(" ❌ Failed to generate query");
|
||
}
|
||
|
||
if (i < demoQueries.length - 1) {
|
||
console.log(" " + "-".repeat(50));
|
||
}
|
||
}
|
||
|
||
if (!executeQueries) {
|
||
console.log(`\n💡 To execute queries and see results, run:`);
|
||
console.log(` EXECUTE_DEMO=true node demo.js`);
|
||
}
|
||
}
|
||
|
||
console.log("\n📊 ODMDB Status Check:");
|
||
|
||
// Check if ODMDB data is accessible
|
||
const seekersPath = "../smatchitObjectOdmdb/objects/seekers/itm";
|
||
try {
|
||
if (fs.existsSync(seekersPath)) {
|
||
const files = fs
|
||
.readdirSync(seekersPath)
|
||
.filter((f) => f.endsWith(".json") && f !== "backup");
|
||
console.log(`✅ Found ${files.length} seeker files in ${seekersPath}`);
|
||
|
||
// Sample a few files to show data types
|
||
const sampleFile = files[0];
|
||
const sampleData = JSON.parse(
|
||
fs.readFileSync(`${seekersPath}/${sampleFile}`, "utf-8")
|
||
);
|
||
console.log(`📄 Sample seeker data (${sampleFile}):`);
|
||
console.log(` - alias: ${sampleData.alias}`);
|
||
console.log(` - email: ${sampleData.email}`);
|
||
console.log(` - seekstatus: ${sampleData.seekstatus}`);
|
||
console.log(` - seekworkingyear: ${sampleData.seekworkingyear}`);
|
||
console.log(` - dt_create: ${sampleData.dt_create}`);
|
||
} else {
|
||
console.log(`❌ ODMDB data not found at ${seekersPath}`);
|
||
}
|
||
} catch (error) {
|
||
console.log(`❌ Error accessing ODMDB data: ${error.message}`);
|
||
}
|
||
|
||
const schemaPath = "../smatchitObjectOdmdb/schema/seekers.json";
|
||
try {
|
||
if (fs.existsSync(schemaPath)) {
|
||
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
|
||
const fieldCount = Object.keys(schema.properties || {}).length;
|
||
console.log(`✅ Loaded seekers schema with ${fieldCount} properties`);
|
||
|
||
// Show access rights info
|
||
if (schema.apxaccessrights?.recruiters?.R) {
|
||
console.log(
|
||
`📋 Recruiter-readable fields: ${schema.apxaccessrights.recruiters.R.slice(
|
||
0,
|
||
5
|
||
).join(", ")}... (${schema.apxaccessrights.recruiters.R.length} total)`
|
||
);
|
||
}
|
||
|
||
// Show available indexes
|
||
if (schema.apxidx) {
|
||
const indexes = schema.apxidx.map((idx) => idx.name);
|
||
console.log(`🔍 Available indexes: ${indexes.join(", ")}`);
|
||
}
|
||
} else {
|
||
console.log(`❌ Schema not found at ${schemaPath}`);
|
||
}
|
||
} catch (error) {
|
||
console.log(`❌ Error loading schema: ${error.message}`);
|
||
}
|
||
|
||
console.log("\n🚀 Running Live PoC Demo...");
|
||
runDemo()
|
||
.then(() => {
|
||
console.log("\n✅ Demo complete!");
|
||
})
|
||
.catch((error) => {
|
||
console.error("\n❌ Demo failed:", error.message);
|
||
process.exit(1);
|
||
});
|