Files
tilbudgivern/carpenter-agent-openai.js
2026-07-05 08:10:59 +00:00

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();