788 lines
28 KiB
JavaScript
788 lines
28 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* 🔨 CARPENTER AGENT - OpenAI Powered
|
|
*
|
|
* An intelligent AI-powered carpenter agent that:
|
|
* - Uses OpenAI GPT-4 to think and decide like a real carpenter
|
|
* - Manages multi-turn conversations with natural language
|
|
* - Creates realistic and contextual project quotes
|
|
* - Learns and adapts based on previous interactions
|
|
* - Stores all interactions and quotes for analysis
|
|
*
|
|
* Usage:
|
|
* node carpenter-agent-openai.js [--interactive] [--carpenter=lars]
|
|
* node carpenter-agent-openai.js --generate-quotes=5 [--save]
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const readline = require('readline');
|
|
const os = require('os');
|
|
const { execFileSync } = require('child_process');
|
|
|
|
const CODEX_MODEL = process.env.CODEX_MODEL || 'gpt-5.4-mini';
|
|
|
|
function runCodexPrompt(prompt) {
|
|
const outputFile = path.join(
|
|
os.tmpdir(),
|
|
`codex-carpenter-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`
|
|
);
|
|
|
|
try {
|
|
execFileSync(
|
|
'codex',
|
|
[
|
|
'exec',
|
|
'-m',
|
|
CODEX_MODEL,
|
|
'--color',
|
|
'never',
|
|
'--output-last-message',
|
|
outputFile,
|
|
prompt
|
|
],
|
|
{
|
|
cwd: process.cwd(),
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
encoding: 'utf8',
|
|
maxBuffer: 10 * 1024 * 1024
|
|
}
|
|
);
|
|
|
|
return fs.readFileSync(outputFile, 'utf8').trim();
|
|
} finally {
|
|
if (fs.existsSync(outputFile)) {
|
|
fs.unlinkSync(outputFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Carpenter personas with detailed backgrounds
|
|
const CARPENTER_PERSONAS = {
|
|
lars: {
|
|
name: 'Lars Nielsen',
|
|
experience: '15 år',
|
|
specialty: 'Tag og terrasser',
|
|
phone: '40 40 40 40',
|
|
email: 'lars@tommer.dk',
|
|
description: `Du er Lars Nielsen, en erfaren tømrer med 15 års erfaring, især inden for tag og terrasser.
|
|
Du arbejder selvstændigt og kender alle aspekter af dit arbejde - fra prisbedømmelse til projektplanlægning.
|
|
Du er kendt for dine nøjagtige tilbud og høj kvalitet. Du har stærke meninger om materialevalg og bygningspraksis.
|
|
Du taler naturligt dansk og giver praktisk information baseret på rigtig erfaring.`,
|
|
specialties: ['Tagdækning', 'Terrasse', 'Stråtag', 'Reparationer']
|
|
},
|
|
jannick: {
|
|
name: 'Jannick Andersen',
|
|
experience: '12 år',
|
|
specialty: 'Indendørs renovering',
|
|
phone: '41 41 41 41',
|
|
email: 'jannick@tommer.dk',
|
|
description: `Du er Jannick Andersen, en specialiseret tømrer med 12 års erfaring inden for indendørs renovering.
|
|
Du elsker at transformere rum og arbejder meget med møbler, gulve og indvendige installationer.
|
|
Du er dygtig til at høre på kunders behov og finde kreative løsninger.
|
|
Du vejleder kunderne gerne om stilarter og materialevalg for forskellige stilarter.`,
|
|
specialties: ['Gulv', 'Møbler', 'Garderobeløsninger', 'Køkkenarbejde']
|
|
},
|
|
alexander: {
|
|
name: 'Alexander Ørneby Andersen',
|
|
experience: '8 år',
|
|
specialty: 'Moderne bygge- og renoveringsprojekter',
|
|
phone: '42 42 42 42',
|
|
email: 'alexander@tommer.dk',
|
|
description: `Du er Alexander Ørneby Andersen, en moderne tømrer med 8 års erfaring inden for nyere bygge- og renoveringsprojekter.
|
|
Du arbejder gerne med nye teknologier og materialer. Du har stor interesse i bæredygtighed og moderne design.
|
|
Du er dygtig til at kommunikere komplekse ideer enkelt til kunder.
|
|
Du holder dig ajour med nye byggenormer og bedste praksis.`,
|
|
specialties: ['Moderne renovering', 'Bæredygtige materialer', 'Dørinstallation', 'Vinduesbeklædning']
|
|
}
|
|
};
|
|
|
|
// System prompts for different carpenter roles
|
|
function getSystemPrompt(carpenter) {
|
|
return `${carpenter.description}
|
|
|
|
Du er i en kundesamtale hvor kunder spørger om deres projekter og ønsker tilbud.
|
|
|
|
Din rolle:
|
|
1. Høre omhyggeligt på kunden og stille opklarende spørgsmål
|
|
2. Give praktisk vejledning baseret på din erfaring
|
|
3. Estimere realistiske priser og tidslinjer
|
|
4. Være professionel men venlig og tilgængelig
|
|
5. Dokumentere projektdetaljer for senere tilbudsforberedelse
|
|
|
|
Når du har nok information, præsenterer du et grovindblik af tilbuddet i følgende format:
|
|
---QUOTE_SUMMARY---
|
|
PROJEKT: [Kort navn]
|
|
BESKRIVELSE: [Kort beskrivelse af arbejdet]
|
|
OMRÅDE: [m² eller lignende mål]
|
|
ESTIMERET VARIGHED: [Tidsestimativ]
|
|
ESTIMERET PRIS: [Pris intervaller i DKK]
|
|
MATERIALER: [Liste af primære materialer]
|
|
NOTER: [Særlige bemærkninger]
|
|
---END_QUOTE---
|
|
|
|
Brug altid dansk. Vær konkret og fagligt nøjagtig.`;
|
|
}
|
|
|
|
/**
|
|
* Run a conversation with an AI carpenter
|
|
*/
|
|
async function carpenterConversation(carpenterId = 'lars', maxTurns = 5) {
|
|
const carpenter = CARPENTER_PERSONAS[carpenterId];
|
|
if (!carpenter) {
|
|
console.error(`❌ Carpenter '${carpenterId}' not found!`);
|
|
return null;
|
|
}
|
|
|
|
console.log(`\n${'='.repeat(80)}`);
|
|
console.log(`🔨 CARPENTER AGENT: ${carpenter.name}`);
|
|
console.log(`📍 Specialty: ${carpenter.specialty}`);
|
|
console.log(`Experience: ${carpenter.experience}`);
|
|
console.log(`${'='.repeat(80)}\n`);
|
|
|
|
const conversationHistory = [];
|
|
let turnCount = 0;
|
|
let extractedQuote = null;
|
|
|
|
// Initial greeting from carpenter
|
|
const initialMessage = `Hej! Jeg er ${carpenter.name}.
|
|
Jeg har ${carpenter.experience} erfaring inden for ${carpenter.specialty.toLowerCase()}.
|
|
Hvordan kan jeg hjælpe dig i dag? Hvad for et projekt er du ved at planlægge?`;
|
|
|
|
console.log(`🔨 ${carpenter.name}: ${initialMessage}\n`);
|
|
conversationHistory.push({
|
|
role: 'assistant',
|
|
content: initialMessage
|
|
});
|
|
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout
|
|
});
|
|
|
|
const askQuestion = (question) => {
|
|
return new Promise((resolve) => {
|
|
rl.question(question, (answer) => {
|
|
resolve(answer);
|
|
});
|
|
});
|
|
};
|
|
|
|
// Conversation loop
|
|
while (turnCount < maxTurns) {
|
|
const userInput = await askQuestion('👤 Du: ');
|
|
|
|
if (userInput.toLowerCase() === 'exit' || userInput.toLowerCase() === 'quit') {
|
|
console.log('\n👋 Farvel!');
|
|
break;
|
|
}
|
|
|
|
turnCount++;
|
|
conversationHistory.push({
|
|
role: 'user',
|
|
content: userInput
|
|
});
|
|
|
|
try {
|
|
console.log('\n⏳ Carpenter is thinking...');
|
|
|
|
const prompt = [
|
|
getSystemPrompt(carpenter),
|
|
'',
|
|
'Samtalehistorik:',
|
|
...conversationHistory.map(msg => `${msg.role.toUpperCase()}: ${msg.content}`),
|
|
'',
|
|
'Svar som assistent i samme samtale.'
|
|
].join('\n');
|
|
const assistantMessage = runCodexPrompt(prompt);
|
|
conversationHistory.push({
|
|
role: 'assistant',
|
|
content: assistantMessage
|
|
});
|
|
|
|
console.log(`\n🔨 ${carpenter.name}: ${assistantMessage}\n`);
|
|
|
|
// Check if a quote was generated
|
|
if (assistantMessage.includes('---QUOTE_SUMMARY---')) {
|
|
extractedQuote = extractQuote(assistantMessage, carpenter);
|
|
console.log('\n✅ Quote generated! Saving...');
|
|
break;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error communicating with OpenAI:', error.message);
|
|
break;
|
|
}
|
|
}
|
|
|
|
rl.close();
|
|
|
|
return {
|
|
carpenter,
|
|
conversationHistory,
|
|
extractedQuote,
|
|
turns: turnCount
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract quote data from assistant message
|
|
*/
|
|
function extractQuote(message, carpenter) {
|
|
const quoteMatch = message.match(/---QUOTE_SUMMARY---([\s\S]*?)---END_QUOTE---/);
|
|
if (!quoteMatch) return null;
|
|
|
|
const quoteText = quoteMatch[1];
|
|
const lines = quoteText.split('\n').filter(l => l.trim());
|
|
|
|
const quote = {
|
|
timestamp: new Date().toISOString(),
|
|
carpenter: {
|
|
name: carpenter.name,
|
|
experience: carpenter.experience,
|
|
specialty: carpenter.specialty,
|
|
email: carpenter.email
|
|
}
|
|
};
|
|
|
|
for (const line of lines) {
|
|
if (line.includes('PROJEKT:')) {
|
|
quote.projectName = line.split('PROJEKT:')[1].trim();
|
|
} else if (line.includes('BESKRIVELSE:')) {
|
|
quote.description = line.split('BESKRIVELSE:')[1].trim();
|
|
} else if (line.includes('OMRÅDE:')) {
|
|
quote.area = line.split('OMRÅDE:')[1].trim();
|
|
} else if (line.includes('ESTIMERET VARIGHED:')) {
|
|
quote.timeline = line.split('ESTIMERET VARIGHED:')[1].trim();
|
|
} else if (line.includes('ESTIMERET PRIS:')) {
|
|
quote.price = line.split('ESTIMERET PRIS:')[1].trim();
|
|
} else if (line.includes('MATERIALER:')) {
|
|
quote.materials = line.split('MATERIALER:')[1].trim();
|
|
} else if (line.includes('NOTER:')) {
|
|
quote.notes = line.split('NOTER:')[1].trim();
|
|
}
|
|
}
|
|
|
|
return quote;
|
|
}
|
|
|
|
/**
|
|
* Generate quotes programmatically (non-interactive)
|
|
*/
|
|
async function generateQuotesAI(count = 3, saveToDisk = true) {
|
|
console.log(`\n${'='.repeat(80)}`);
|
|
console.log(`🤖 GENERATING ${count} QUOTES WITH AI CARPENTER AGENTS`);
|
|
console.log(`${'='.repeat(80)}\n`);
|
|
|
|
const generatedQuotes = [];
|
|
const carpenterIds = Object.keys(CARPENTER_PERSONAS);
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const carpenterId = carpenterIds[i % carpenterIds.length];
|
|
const carpenter = CARPENTER_PERSONAS[carpenterId];
|
|
|
|
console.log(`\n📝 Quote ${i + 1}/${count}`);
|
|
console.log(`🔨 Carpenter: ${carpenter.name}`);
|
|
console.log('⏳ Generating...');
|
|
|
|
try {
|
|
const quotePrompt = [
|
|
getSystemPrompt(carpenter),
|
|
'',
|
|
`Kunde: ${generateRandomProjectPrompt(carpenter)}`,
|
|
'',
|
|
'Giv dit svar nu.'
|
|
].join('\n');
|
|
const quoteText = runCodexPrompt(quotePrompt);
|
|
const quote = extractQuote(quoteText, carpenter) || {
|
|
carpenter: {
|
|
name: carpenter.name,
|
|
experience: carpenter.experience,
|
|
specialty: carpenter.specialty
|
|
},
|
|
rawResponse: quoteText
|
|
};
|
|
|
|
generatedQuotes.push(quote);
|
|
console.log(`✅ Quote created: ${quote.projectName || 'AI Generated'}`);
|
|
|
|
// Add delay to respect API rate limits
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
} catch (error) {
|
|
console.error(`❌ Error generating quote ${i + 1}:`, error.message);
|
|
}
|
|
}
|
|
|
|
// Save if requested
|
|
if (saveToDisk) {
|
|
const outputDir = 'test-results/carpenter-openai-data';
|
|
if (!fs.existsSync(outputDir)) {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
}
|
|
|
|
const timestamp = new Date().toISOString().split('T')[0];
|
|
const filename = path.join(outputDir, `ai-quotes-${timestamp}.json`);
|
|
fs.writeFileSync(filename, JSON.stringify(generatedQuotes, null, 2));
|
|
|
|
console.log(`\n✅ Quotes saved to: ${filename}`);
|
|
}
|
|
|
|
return generatedQuotes;
|
|
}
|
|
|
|
/**
|
|
* Personality-specific feedback test prompts
|
|
*/
|
|
function getPersonalityFeedbackPrompts(carpenterId) {
|
|
const prompts = {
|
|
lars: [
|
|
'Kunden spørger: Kan du give ærlig og kritisk feedback på min tagløsning og især opsætningen omkring skorstenen? Fokusér på fejl og brugervenlighed i vedligehold.',
|
|
'Kunden spørger: Giv en kritisk gennemgang af opsætning og brugervenlighed for en ny træterrasse på 24 m² med lav vedligeholdelse.'
|
|
],
|
|
jannick: [
|
|
'Kunden spørger: Vær kritisk på opsætning og daglig brugervenlighed i valget mellem parket og klikgulv i stue og gang med børn.',
|
|
'Kunden spørger: Giv kritisk feedback på en garderobeløsning i et lille soveværelse med fokus på dårlig pladsudnyttelse og brugerfejl.'
|
|
],
|
|
alexander: [
|
|
'Kunden spørger: Giv kritisk feedback på min opsætning af bæredygtige materialer i moderne renovering, især hvor løsningen er upraktisk i drift.',
|
|
'Kunden spørger: Vær meget kritisk på opsætning og brugervenlighed ved nye dørinstallationer i et åbent køkken.'
|
|
]
|
|
};
|
|
|
|
return prompts[carpenterId] || prompts.lars;
|
|
}
|
|
|
|
/**
|
|
* Validate if response appears aligned with the carpenter personality
|
|
*/
|
|
function validatePersonalityFeedback(responseText, carpenter) {
|
|
const text = (responseText || '').toLowerCase();
|
|
const specialtyTokens = carpenter.specialties.map(s => s.toLowerCase());
|
|
|
|
const hasSpecialtySignal = specialtyTokens.some(token => text.includes(token));
|
|
const hasConcreteGuidance = ['anbefal', 'foreslår', 'vigtigt', 'du bør', 'jeg vil', 'pris', 'tids'].some(token => text.includes(token));
|
|
const hasCriticalTone = ['kritisk', 'problem', 'fejl', 'risiko', 'svaghed', 'mangel', 'ikke optimalt', 'bør undgå'].some(token => text.includes(token));
|
|
const likelyDanish = ['jeg', 'du', 'og', 'med', 'til', 'er'].filter(token => text.includes(token)).length >= 3;
|
|
|
|
const checks = {
|
|
hasSpecialtySignal,
|
|
hasConcreteGuidance,
|
|
hasCriticalTone,
|
|
likelyDanish
|
|
};
|
|
|
|
const passed = Object.values(checks).every(Boolean);
|
|
return { passed, checks };
|
|
}
|
|
|
|
function getTilbudgivernSiteReviewContext() {
|
|
return [
|
|
'Du skal reviewe webappen Tilbudgivern som produkt for en rigtig tømrer.',
|
|
'Formålet med Tilbudgivern er at hjælpe en tømrer med hurtigt at oprette et brugbart tilbud til en kunde.',
|
|
'Centrale trin i flowet er:',
|
|
'1. Login',
|
|
'2. Projektoprettelse med projekt- og kundedata',
|
|
'3. Geometri/mål',
|
|
'4. Smart Pakke/materialer/timer',
|
|
'5. Final Review og afsendelse til Ordrestyring',
|
|
'Observerede problemer fra browser-test:',
|
|
'- Loginfelter blev fundet efter forbedrede placeholders og labels',
|
|
'- Efter login kunne agenten udfylde projekt- og kundefelter',
|
|
'- Flowet blev derefter hængende i projektoprettelsen og gik ikke tydeligt videre til næste trin',
|
|
'- Det var uklart hvilken knap/handling der skulle bruges for at fortsætte',
|
|
'- Final Review er blevet strammet, så tilbud ikke kan sendes uden centrale datafelter',
|
|
'- Opgavenavne i kataloget har haft tegn på ujævn kvalitet og normalisering',
|
|
'Du skal IKKE vurdere tagkonstruktioner eller byggeløsninger.',
|
|
'Du skal vurdere sitets brugervenlighed, datakvalitet og egnethed til rigtige tilbud.'
|
|
].join('\n');
|
|
}
|
|
|
|
function getSiteReviewPrompts(carpenterId) {
|
|
const prompts = {
|
|
lars: [
|
|
'Review Tilbudgivern som en erfaren tagtømrer. Giver knapper, felter og rækkefølge mening, når jeg skal lave et hurtigt tilbud ude hos kunden?',
|
|
'Review om Tilbudgivern har de rigtige datafelter og opgavetyper til realistiske tag- og terrassetilbud. Hvad mangler eller virker forkert?'
|
|
],
|
|
jannick: [
|
|
'Review Tilbudgivern som en tømrer der laver indendørs renovering. Er flowet let at bruge, eller skal jeg tænke for meget undervejs?',
|
|
'Review om materialer, timer, kundeoplysninger og opgavevalg er konkrete nok til gulv, garderobe og køkkenarbejde.'
|
|
],
|
|
alexander: [
|
|
'Review Tilbudgivern som en moderne tømrer. Er systemet robust og professionelt nok til rigtige tilbud, eller føles det halvfærdigt?',
|
|
'Review om systemet virker komplet på tværs af opgaver, eller om der mangler håndværkerkategorier, validering og tilbudslogik.'
|
|
]
|
|
};
|
|
|
|
return prompts[carpenterId] || prompts.lars;
|
|
}
|
|
|
|
function validateSiteReview(responseText) {
|
|
const text = (responseText || '').toLowerCase();
|
|
const checks = {
|
|
mentionsUiOrFlow: ['knap', 'felt', 'flow', 'trin', 'formular', 'bruger'].some(token => text.includes(token)),
|
|
mentionsQuoteReadiness: ['tilbud', 'pris', 'timer', 'material', 'kunde'].some(token => text.includes(token)),
|
|
hasCriticalTone: ['kritisk', 'problem', 'fejl', 'mangel', 'risiko', 'svaghed', 'uklart'].some(token => text.includes(token)),
|
|
hasActionableFixes: ['bør', 'skal', 'anbefal', 'foreslår', 'mangler'].some(token => text.includes(token))
|
|
};
|
|
|
|
return {
|
|
passed: Object.values(checks).every(Boolean),
|
|
checks
|
|
};
|
|
}
|
|
|
|
async function reviewTilbudgivernSite(saveToDisk = true) {
|
|
console.log(`\n${'='.repeat(80)}`);
|
|
console.log('🔎 REVIEWING TILBUDGIVERN AS A CARPENTER PRODUCT');
|
|
console.log(`${'='.repeat(80)}\n`);
|
|
|
|
const reviewResults = [];
|
|
const siteContext = getTilbudgivernSiteReviewContext();
|
|
|
|
for (const [carpenterId, carpenter] of Object.entries(CARPENTER_PERSONAS)) {
|
|
console.log(`\n🔨 Reviewing as: ${carpenter.name} (${carpenter.specialty})`);
|
|
const prompts = getSiteReviewPrompts(carpenterId);
|
|
|
|
for (let i = 0; i < prompts.length; i++) {
|
|
const userPrompt = prompts[i];
|
|
console.log(` 📝 Prompt ${i + 1}/${prompts.length}`);
|
|
|
|
try {
|
|
const reviewPrompt = [
|
|
getSystemPrompt(carpenter),
|
|
'',
|
|
siteContext,
|
|
'',
|
|
userPrompt,
|
|
'Krav til svar:',
|
|
'- Review sitet som produkt, ikke håndværksløsningen.',
|
|
'- Vurder knapper, datafelter, rækkefølge, manglende opgaver og om output kan bruges til rigtige tilbud.',
|
|
'- Nævn mindst 3 konkrete problemer eller mangler.',
|
|
'- Giv konkrete forbedringer.',
|
|
'- Svar i 6-10 sætninger uden quote-format.'
|
|
].join('\n');
|
|
|
|
const assistantText = runCodexPrompt(reviewPrompt);
|
|
const validation = validateSiteReview(assistantText);
|
|
|
|
reviewResults.push({
|
|
timestamp: new Date().toISOString(),
|
|
carpenterId,
|
|
carpenterName: carpenter.name,
|
|
specialty: carpenter.specialty,
|
|
prompt: userPrompt,
|
|
response: assistantText,
|
|
validation
|
|
});
|
|
|
|
console.log(` ${validation.passed ? '✅' : '⚠️'} Site review ${validation.passed ? 'passed' : 'needs review'}`);
|
|
await new Promise(resolve => setTimeout(resolve, 800));
|
|
} catch (error) {
|
|
console.error(` ❌ Failed: ${error.message}`);
|
|
reviewResults.push({
|
|
timestamp: new Date().toISOString(),
|
|
carpenterId,
|
|
carpenterName: carpenter.name,
|
|
specialty: carpenter.specialty,
|
|
prompt: userPrompt,
|
|
error: error.message,
|
|
validation: { passed: false, checks: { apiCall: false } }
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const summary = createFeedbackTestSummary(reviewResults);
|
|
console.log(`\n${'='.repeat(80)}`);
|
|
console.log('📊 TILBUDGIVERN SITE REVIEW SUMMARY');
|
|
console.log(`${'='.repeat(80)}`);
|
|
console.log(`Total reviews: ${summary.totalTests}`);
|
|
console.log(`Passed: ${summary.passed}`);
|
|
console.log(`Failed: ${summary.failed}`);
|
|
console.log(`Pass rate: ${summary.passRate}`);
|
|
|
|
for (const [carpenterId, data] of Object.entries(summary.byCarpenter)) {
|
|
console.log(`- ${data.carpenterName} (${carpenterId}): ${data.passed}/${data.total}`);
|
|
}
|
|
|
|
if (saveToDisk) {
|
|
const outputDir = 'test-results/carpenter-openai-data';
|
|
if (!fs.existsSync(outputDir)) {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
}
|
|
|
|
const summaryFile = path.join(outputDir, `site-review-summary-${Date.now()}.json`);
|
|
const detailsFile = path.join(outputDir, `site-review-results-${Date.now()}.json`);
|
|
|
|
fs.writeFileSync(summaryFile, JSON.stringify(summary, null, 2));
|
|
fs.writeFileSync(detailsFile, JSON.stringify(reviewResults, null, 2));
|
|
|
|
console.log(`\n💾 Summary saved: ${summaryFile}`);
|
|
console.log(`💾 Detailed results saved: ${detailsFile}`);
|
|
}
|
|
|
|
return { summary, reviewResults };
|
|
}
|
|
|
|
/**
|
|
* Run personality feedback tests and summarize outcomes
|
|
*/
|
|
async function testPersonalityFeedback(saveToDisk = true) {
|
|
console.log(`\n${'='.repeat(80)}`);
|
|
console.log('🧪 TESTING FEEDBACK BY CARPENTER PERSONALITY');
|
|
console.log(`${'='.repeat(80)}\n`);
|
|
|
|
const testResults = [];
|
|
|
|
for (const [carpenterId, carpenter] of Object.entries(CARPENTER_PERSONAS)) {
|
|
console.log(`\n🔨 Testing: ${carpenter.name} (${carpenter.specialty})`);
|
|
const prompts = getPersonalityFeedbackPrompts(carpenterId);
|
|
|
|
for (let i = 0; i < prompts.length; i++) {
|
|
const userPrompt = prompts[i];
|
|
console.log(` 📝 Prompt ${i + 1}/${prompts.length}`);
|
|
|
|
try {
|
|
const feedbackPrompt = [
|
|
getSystemPrompt(carpenter),
|
|
'',
|
|
`${userPrompt}`,
|
|
'Krav til svar:',
|
|
'- Vær kritisk og peg tydeligt på svagheder i opsætning og brugervenlighed.',
|
|
'- Nævn mindst 2 konkrete fejl/risici.',
|
|
'- Giv derefter konkrete forbedringsforslag.',
|
|
'- Svar i 5-8 sætninger uden quote-format.'
|
|
].join('\n');
|
|
const assistantText = runCodexPrompt(feedbackPrompt);
|
|
const validation = validatePersonalityFeedback(assistantText, carpenter);
|
|
|
|
testResults.push({
|
|
timestamp: new Date().toISOString(),
|
|
carpenterId,
|
|
carpenterName: carpenter.name,
|
|
specialty: carpenter.specialty,
|
|
prompt: userPrompt,
|
|
response: assistantText,
|
|
validation
|
|
});
|
|
|
|
console.log(` ${validation.passed ? '✅' : '⚠️'} Feedback test ${validation.passed ? 'passed' : 'needs review'}`);
|
|
await new Promise(resolve => setTimeout(resolve, 800));
|
|
} catch (error) {
|
|
console.error(` ❌ Failed: ${error.message}`);
|
|
testResults.push({
|
|
timestamp: new Date().toISOString(),
|
|
carpenterId,
|
|
carpenterName: carpenter.name,
|
|
specialty: carpenter.specialty,
|
|
prompt: userPrompt,
|
|
error: error.message,
|
|
validation: { passed: false, checks: { apiCall: false } }
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const summary = createFeedbackTestSummary(testResults);
|
|
printFeedbackTestSummary(summary);
|
|
|
|
if (saveToDisk) {
|
|
const outputDir = 'test-results/carpenter-openai-data';
|
|
if (!fs.existsSync(outputDir)) {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
}
|
|
|
|
const summaryFile = path.join(outputDir, `personality-feedback-summary-${Date.now()}.json`);
|
|
const detailsFile = path.join(outputDir, `personality-feedback-results-${Date.now()}.json`);
|
|
|
|
fs.writeFileSync(summaryFile, JSON.stringify(summary, null, 2));
|
|
fs.writeFileSync(detailsFile, JSON.stringify(testResults, null, 2));
|
|
|
|
console.log(`\n💾 Summary saved: ${summaryFile}`);
|
|
console.log(`💾 Detailed results saved: ${detailsFile}`);
|
|
}
|
|
|
|
return { summary, testResults };
|
|
}
|
|
|
|
function createFeedbackTestSummary(results) {
|
|
const total = results.length;
|
|
const passed = results.filter(r => r.validation && r.validation.passed).length;
|
|
const failed = total - passed;
|
|
|
|
const byCarpenter = {};
|
|
for (const result of results) {
|
|
if (!byCarpenter[result.carpenterId]) {
|
|
byCarpenter[result.carpenterId] = {
|
|
carpenterName: result.carpenterName,
|
|
specialty: result.specialty,
|
|
total: 0,
|
|
passed: 0,
|
|
failed: 0
|
|
};
|
|
}
|
|
|
|
byCarpenter[result.carpenterId].total += 1;
|
|
if (result.validation && result.validation.passed) {
|
|
byCarpenter[result.carpenterId].passed += 1;
|
|
} else {
|
|
byCarpenter[result.carpenterId].failed += 1;
|
|
}
|
|
}
|
|
|
|
return {
|
|
generatedAt: new Date().toISOString(),
|
|
totalTests: total,
|
|
passed,
|
|
failed,
|
|
passRate: total > 0 ? `${((passed / total) * 100).toFixed(1)}%` : '0.0%',
|
|
byCarpenter
|
|
};
|
|
}
|
|
|
|
function printFeedbackTestSummary(summary) {
|
|
console.log(`\n${'='.repeat(80)}`);
|
|
console.log('📊 PERSONALITY FEEDBACK TEST SUMMARY');
|
|
console.log(`${'='.repeat(80)}`);
|
|
console.log(`Total tests: ${summary.totalTests}`);
|
|
console.log(`Passed: ${summary.passed}`);
|
|
console.log(`Failed: ${summary.failed}`);
|
|
console.log(`Pass rate: ${summary.passRate}`);
|
|
console.log('\nPer carpenter:');
|
|
|
|
for (const [carpenterId, data] of Object.entries(summary.byCarpenter)) {
|
|
console.log(`- ${data.carpenterName} (${carpenterId})`);
|
|
console.log(` Specialty: ${data.specialty}`);
|
|
console.log(` Passed/Total: ${data.passed}/${data.total}`);
|
|
}
|
|
|
|
console.log(`${'='.repeat(80)}\n`);
|
|
}
|
|
|
|
/**
|
|
* Generate random project prompts for AI carpenter
|
|
*/
|
|
function generateRandomProjectPrompt(carpenter) {
|
|
const projects = {
|
|
lars: [
|
|
'Jeg har en 40 m² tagflade på mit hus der har nogle lække steder. Taget er omkring 20 år gammelt.',
|
|
'Jeg ønsker at få bygget en ny terrasse på 24 m² ved siden af huset. Gerne i løst træ.',
|
|
'Mit gamle stråtag skal renoveres. Det er omkring 120 m² og ser slidt ud.'
|
|
],
|
|
jannick: [
|
|
'Vi ønsker at lægge nyt parketgulv i vores stue og gang, omkring 50 m² i alt.',
|
|
'Jeg har brug for en stor garderobe i mit sovværelse, omkring 4 m² med skydedøre.',
|
|
'Køkkenet skal renoveres og vi ønsker nye køkkenskabe og hylder.'
|
|
],
|
|
alexander: [
|
|
'Vi er ved at renovere hele huset og skal have nye dørinstallationer. Omkring 5 dørhuller.',
|
|
'Alle vinduer skal have nye håndværkerspil installeret. Huset har omkring 15 vinduer.',
|
|
'Badeværelset skal helt renoveres - nye vægge, gulv og især håndværkerarbejde.'
|
|
]
|
|
};
|
|
|
|
const carpenterId = Object.keys(CARPENTER_PERSONAS).find(
|
|
id => CARPENTER_PERSONAS[id].name === carpenter.name
|
|
);
|
|
|
|
const projectList = projects[carpenterId] || projects.lars;
|
|
return projectList[Math.floor(Math.random() * projectList.length)];
|
|
}
|
|
|
|
/**
|
|
* Main entry point
|
|
*/
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
const isInteractive = args.includes('--interactive') || args.includes('-i');
|
|
const carpenterArg = args.find(a => a.startsWith('--carpenter='));
|
|
const carpenterId = carpenterArg ? carpenterArg.split('=')[1] : 'lars';
|
|
const generateQuotesArg = args.find(a => a.startsWith('--generate-quotes='));
|
|
const quoteCount = generateQuotesArg ? parseInt(generateQuotesArg.split('=')[1]) : 0;
|
|
const runFeedbackTests = args.includes('--test-personality-feedback');
|
|
const runSiteReview = args.includes('--review-site');
|
|
const shouldSave = args.includes('--save') || args.includes('-s');
|
|
|
|
try {
|
|
if (isInteractive) {
|
|
// Interactive mode
|
|
const result = await carpenterConversation(carpenterId);
|
|
|
|
if (result?.extractedQuote) {
|
|
const outputDir = 'test-results/carpenter-openai-data';
|
|
if (!fs.existsSync(outputDir)) {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
}
|
|
|
|
const filename = path.join(
|
|
outputDir,
|
|
`quote-${result.carpenter.name.replace(/\s+/g, '-')}-${Date.now()}.json`
|
|
);
|
|
fs.writeFileSync(filename, JSON.stringify(result.extractedQuote, null, 2));
|
|
console.log(`\n✅ Quote saved to: ${filename}`);
|
|
}
|
|
} else if (quoteCount > 0) {
|
|
// Programmatic quote generation
|
|
const quotes = await generateQuotesAI(quoteCount, shouldSave);
|
|
|
|
console.log(`\n${'='.repeat(80)}`);
|
|
console.log(`✅ GENERATED ${quotes.length} QUOTES`);
|
|
console.log(`${'='.repeat(80)}\n`);
|
|
|
|
quotes.forEach((q, idx) => {
|
|
console.log(`${idx + 1}. ${q.projectName || 'AI Generated Project'}`);
|
|
console.log(` Carpenter: ${q.carpenter.name}`);
|
|
if (q.price) console.log(` Price: ${q.price}`);
|
|
if (q.timeline) console.log(` Timeline: ${q.timeline}`);
|
|
});
|
|
} else if (runFeedbackTests) {
|
|
await testPersonalityFeedback(shouldSave);
|
|
} else if (runSiteReview) {
|
|
await reviewTilbudgivernSite(shouldSave);
|
|
} else {
|
|
// Default: Show help
|
|
console.log(`
|
|
🔨 CARPENTER AGENT - OpenAI Powered
|
|
|
|
Usage:
|
|
# Interactive mode with specific carpenter
|
|
node carpenter-agent-openai.js --interactive --carpenter=lars
|
|
|
|
# Generate 5 quotes automatically
|
|
node carpenter-agent-openai.js --generate-quotes=5 --save
|
|
|
|
# Test personality-based feedback quality
|
|
node carpenter-agent-openai.js --test-personality-feedback --save
|
|
|
|
# Review Tilbudgivern as a carpenter product
|
|
node carpenter-agent-openai.js --review-site --save
|
|
|
|
# Available carpenters: lars, jannick, alexander
|
|
|
|
Options:
|
|
--interactive, -i Interactive conversation mode
|
|
--carpenter=NAME Choose carpenter (lars, jannick, alexander)
|
|
--generate-quotes=N Generate N quotes programmatically
|
|
--test-personality-feedback Run personality-specific feedback tests
|
|
--review-site Review Tilbudgivern as a carpenter-facing product
|
|
--save, -s Save generated data to disk
|
|
|
|
Examples:
|
|
node carpenter-agent-openai.js -i --carpenter=jannick
|
|
node carpenter-agent-openai.js --generate-quotes=10 --save
|
|
node carpenter-agent-openai.js --test-personality-feedback --save
|
|
node carpenter-agent-openai.js --review-site --save
|
|
|
|
Optional: CODEX_MODEL environment variable (default: gpt-5.4-mini)
|
|
`);
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Error:', error.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|