Files
tilbudgivern/carpenter-agent-openai.js

434 lines
14 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 OpenAI = require('openai');
const fs = require('fs');
const path = require('path');
const readline = require('readline');
// Initialize OpenAI client
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY || ''
});
// Check API key
if (!process.env.OPENAI_API_KEY) {
console.error('❌ Error: OPENAI_API_KEY environment variable is not set!');
console.error('\nSet it with:');
console.error(' export OPENAI_API_KEY="sk-..."');
process.exit(1);
}
// 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 response = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{
role: 'system',
content: getSystemPrompt(carpenter)
},
...conversationHistory
],
temperature: 0.7,
max_tokens: 1000
});
const assistantMessage = response.choices[0].message.content;
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 messages = [
{
role: 'system',
content: getSystemPrompt(carpenter)
},
{
role: 'user',
content: generateRandomProjectPrompt(carpenter)
}
];
const response = await openai.chat.completions.create({
model: 'gpt-4',
messages,
temperature: 0.8,
max_tokens: 1500
});
const quoteText = response.choices[0].message.content;
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;
}
/**
* 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 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 {
// 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
# 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
--save, -s Save generated quotes to disk
Examples:
node carpenter-agent-openai.js -i --carpenter=jannick
node carpenter-agent-openai.js --generate-quotes=10 --save
Requires: OPENAI_API_KEY environment variable
`);
}
} catch (error) {
console.error('❌ Error:', error.message);
process.exit(1);
}
}
main();