PowerUp / server.js
priyaiyer's picture
Upload 11 files
608f7b3 verified
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const path = require('path');
const multer = require('multer');
const { v4: uuidv4 } = require('uuid');
const { ClaudePricingAgent } = require('./ai-pricing-agent');
const { ClaudeTradesMatchingAgent } = require('./claude-matching-agent');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static('public'));
// File upload configuration
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
cb(null, Date.now() + '-' + file.originalname);
}
});
const upload = multer({ storage: storage });
// Demo database - In production, this would be MongoDB
let workers = [
{
id: 'w1',
name: 'Marcus Thompson',
initials: 'MT',
trade: 'Electrician',
specialties: ['Residential Wiring', 'Panel Upgrades', 'Lighting Installation'],
rating: 4.8,
reviewCount: 167,
distance: 1.2,
hourlyRate: 85,
location: { lat: 37.7749, lng: -122.4194, city: 'San Francisco' },
availability: ['tomorrow', 'next-week'],
certifications: ['Master Electrician License', 'OSHA Certified'],
experience: 12,
completedJobs: 340,
portfolio: [
{ type: 'image', url: '/images/electrical-work-1.jpg', description: 'Panel upgrade' },
{ type: 'image', url: '/images/electrical-work-2.jpg', description: 'Kitchen lighting' }
]
},
{
id: 'w2',
name: 'Rick Williams',
initials: 'RW',
trade: 'Plumber',
specialties: ['Pipe Repair', 'Water Heater Installation', 'Drain Cleaning'],
rating: 4.7,
reviewCount: 203,
distance: 2.8,
hourlyRate: 75,
location: { lat: 37.7849, lng: -122.4094, city: 'San Francisco' },
availability: ['today', 'tomorrow'],
certifications: ['Licensed Plumber', 'Backflow Prevention'],
experience: 8,
completedJobs: 285,
portfolio: [
{ type: 'image', url: '/images/plumbing-work-1.jpg', description: 'Pipe replacement' }
]
},
{
id: 'w3',
name: 'Jake Roberts',
initials: 'JR',
trade: 'Auto Mechanic',
specialties: ['Engine Repair', 'Brake Service', 'Diagnostics'],
rating: 4.8,
reviewCount: 157,
distance: 3.2,
hourlyRate: 95,
location: { lat: 37.7649, lng: -122.4294, city: 'San Francisco' },
availability: ['tomorrow'],
certifications: ['ASE Certified', 'Hybrid Vehicle Specialist'],
experience: 15,
completedJobs: 420,
portfolio: []
},
{
id: 'w4',
name: 'Alex Turner',
initials: 'AT',
trade: 'Auto Mechanic',
specialties: ['Oil Changes', 'Tire Service', 'Basic Maintenance'],
rating: 4.6,
reviewCount: 89,
distance: 4.1,
hourlyRate: 65,
location: { lat: 37.7549, lng: -122.4394, city: 'San Francisco' },
availability: ['tomorrow', 'next-week'],
certifications: ['Basic Auto Repair'],
experience: 5,
completedJobs: 150,
portfolio: []
},
{
id: 'w5',
name: 'Danny Fix',
initials: 'DF',
trade: 'Mobile Mechanic',
specialties: ['On-site Repair', 'Emergency Service', 'Diagnostics'],
rating: 4.7,
reviewCount: 134,
distance: 2.5,
hourlyRate: 90,
location: { lat: 37.7749, lng: -122.4094, city: 'San Francisco' },
availability: ['today', 'tomorrow'],
certifications: ['Mobile Service Certified', 'Emergency Response'],
experience: 10,
completedJobs: 275,
portfolio: []
}
];
let jobs = [];
let conversations = [];
// AI Agent Classes
class TradesMatchingAgent {
constructor() {
this.tradeKeywords = {
'electrician': ['light', 'electrical', 'wire', 'outlet', 'switch', 'power', 'circuit', 'panel'],
'plumber': ['pipe', 'leak', 'water', 'drain', 'toilet', 'sink', 'faucet', 'plumbing'],
'mechanic': ['car', 'engine', 'brake', 'oil', 'tire', 'automotive', 'vehicle', 'repair'],
'hvac': ['heating', 'cooling', 'air', 'furnace', 'ac', 'ventilation', 'hvac'],
'carpenter': ['wood', 'door', 'window', 'cabinet', 'furniture', 'construction']
};
}
analyzeProblem(description, images = []) {
const problem = {
description: description.toLowerCase(),
trades: [],
urgency: 'flexible',
details: {},
confidence: 0
};
// Identify trade types
for (const [trade, keywords] of Object.entries(this.tradeKeywords)) {
const matches = keywords.filter(keyword => problem.description.includes(keyword));
if (matches.length > 0) {
problem.trades.push({
trade: trade,
confidence: matches.length / keywords.length,
matchedKeywords: matches
});
}
}
// Assess urgency
const urgentKeywords = ['emergency', 'urgent', 'asap', 'immediately', 'flooding', 'sparking'];
const soonKeywords = ['today', 'tomorrow', 'soon', 'quickly'];
if (urgentKeywords.some(keyword => problem.description.includes(keyword))) {
problem.urgency = 'emergency';
} else if (soonKeywords.some(keyword => problem.description.includes(keyword))) {
problem.urgency = 'soon';
}
// Sort trades by confidence
problem.trades.sort((a, b) => b.confidence - a.confidence);
problem.confidence = problem.trades.length > 0 ? problem.trades[0].confidence : 0;
return problem;
}
findWorkers(problem, location = null) {
if (problem.trades.length === 0) return [];
const primaryTrade = problem.trades[0].trade;
let matches = workers.filter(worker =>
worker.trade.toLowerCase().includes(primaryTrade) ||
primaryTrade.includes(worker.trade.toLowerCase())
);
// Sort by rating and distance
matches.sort((a, b) => {
const scoreA = a.rating * 0.7 + (5 - a.distance) * 0.3;
const scoreB = b.rating * 0.7 + (5 - b.distance) * 0.3;
return scoreB - scoreA;
});
return matches.slice(0, 4);
}
generateFollowUpQuestions(problem) {
const questions = [];
if (problem.trades.length === 0) {
questions.push("Could you describe the problem in more detail? What specifically needs to be fixed or worked on?");
return questions;
}
const primaryTrade = problem.trades[0].trade;
switch (primaryTrade) {
case 'electrician':
questions.push("Is this related to a specific outlet, light fixture, or your electrical panel?");
questions.push("Are you experiencing any power outages or electrical safety concerns?");
break;
case 'plumber':
questions.push("Is water currently leaking? If so, how much water and where exactly?");
questions.push("Is this affecting your water supply or drainage?");
break;
case 'mechanic':
questions.push("What make and model is your vehicle?");
questions.push("Are you able to drive the car, or does it need to be towed?");
break;
}
if (problem.urgency === 'flexible') {
questions.push("When would you like this work completed?");
}
return questions;
}
}
// Legacy PricingAgent replaced by ClaudePricingAgent
// Initialize AI agents
const matchingAgent = new ClaudeTradesMatchingAgent(process.env.ANTHROPIC_API_KEY);
const pricingAgent = new ClaudePricingAgent(process.env.ANTHROPIC_API_KEY);
// Routes
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Chat API
app.post('/api/chat', async (req, res) => {
const { message, conversationId } = req.body;
let conversation = conversations.find(c => c.id === conversationId);
if (!conversation) {
conversation = {
id: conversationId || uuidv4(),
messages: [],
problem: null,
matches: []
};
conversations.push(conversation);
}
conversation.messages.push({
type: 'user',
content: message,
timestamp: new Date()
});
// Analyze the problem with Claude AI
const problem = await matchingAgent.analyzeProblem(message, [], null);
conversation.problem = problem;
let response = '';
let showMatches = false;
// Check if we need more information before proceeding
if (problem.needsMoreInfo && problem.followUpQuestions && problem.followUpQuestions.length > 0) {
// Ask follow-up questions to gather more details
response = `${problem.summary || 'I understand your situation.'}\n\nTo provide the best help, I need a few more details:\n\n${problem.followUpQuestions.map((q, i) => `${i + 1}. ${q}`).join('\n')}`;
// Add safety warnings if present
if (problem.safetyIssues && problem.safetyIssues.length > 0) {
response += `\n\n⚠️ **Safety Note**: ${problem.safetyIssues.join(', ')}`;
}
} else if (!problem.needsMoreInfo && problem.confidence > 0.3 && problem.trades.length > 0) {
// Proceed with matching if we have enough information
const workerMatching = await matchingAgent.findWorkers(problem, workers, null, {});
// Calculate pricing with Claude AI for each matched worker
const matchesWithPricing = [];
for (const worker of workerMatching.matches) {
try {
const pricing = await pricingAgent.calculatePrice(worker, problem, problem.problemDetails?.timeEstimate ? parseFloat(problem.problemDetails.timeEstimate) : 2);
matchesWithPricing.push({
...worker,
pricing: pricing
});
} catch (error) {
console.error('Pricing error for worker:', worker.id, error);
matchesWithPricing.push({
...worker,
pricing: {
total: worker.hourlyRate * 2,
source: 'fallback',
reasoning: 'Pricing calculation unavailable'
}
});
}
}
conversation.matches = matchesWithPricing;
if (matchesWithPricing.length > 0) {
response = problem.summary || `Great! I found skilled ${problem.trades[0].trade} professionals. Let me show you the best matches for your needs.`;
showMatches = true;
// Add material and time estimates if available
if (problem.problemDetails?.materialEstimate || problem.problemDetails?.timeEstimate) {
response += '\n\n📋 **Estimates:**';
if (problem.problemDetails.materialEstimate) {
response += `\n• Materials: ${problem.problemDetails.materialEstimate}`;
}
if (problem.problemDetails.timeEstimate) {
response += `\n• Time: ${problem.problemDetails.timeEstimate}`;
}
}
} else {
// Generate AI-powered response for no matches found
try {
const noMatchResponse = await matchingAgent.generateNoMatchResponse(problem);
response = noMatchResponse.response;
} catch (error) {
console.error('Error generating no match response:', error);
response = "I couldn't find suitable workers right now. Let me help you refine your request.";
}
}
} else {
// Generate AI-powered clarification questions
try {
const clarificationResponse = await matchingAgent.generateClarificationQuestions(message);
response = clarificationResponse.response;
} catch (error) {
console.error('Error generating clarification questions:', error);
response = "I'd like to help you better. Could you tell me more about what's happening?";
}
}
conversation.messages.push({
type: 'assistant',
content: response,
timestamp: new Date(),
showMatches: showMatches,
matches: showMatches ? conversation.matches : null
});
res.json({
response: response,
conversationId: conversation.id,
showMatches: showMatches,
matches: showMatches ? conversation.matches : null,
problem: problem
});
});
// Get worker details
app.get('/api/workers/:id', (req, res) => {
const worker = workers.find(w => w.id === req.params.id);
if (!worker) {
return res.status(404).json({ error: 'Worker not found' });
}
res.json(worker);
});
// Book a worker
app.post('/api/book', (req, res) => {
const { workerId, date, time, problemDescription, estimatedCost } = req.body;
const worker = workers.find(w => w.id === workerId);
if (!worker) {
return res.status(404).json({ error: 'Worker not found' });
}
const booking = {
id: uuidv4(),
workerId: workerId,
worker: worker,
date: date,
time: time,
problemDescription: problemDescription,
estimatedCost: estimatedCost,
status: 'confirmed',
createdAt: new Date()
};
jobs.push(booking);
res.json({
success: true,
booking: booking,
message: 'Booking confirmed successfully!'
});
});
// Get all workers (for browsing)
app.get('/api/workers', (req, res) => {
const { trade, location } = req.query;
let filteredWorkers = workers;
if (trade) {
filteredWorkers = filteredWorkers.filter(worker =>
worker.trade.toLowerCase().includes(trade.toLowerCase())
);
}
res.json(filteredWorkers);
});
// Image upload endpoint
app.post('/api/upload-image', upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No image file provided' });
}
const { problemContext = '' } = req.body;
// Convert image to base64 for Claude Vision API
const fs = require('fs');
const imageBuffer = fs.readFileSync(req.file.path);
const base64Image = imageBuffer.toString('base64');
try {
// Analyze image with Claude Vision
const analysis = await matchingAgent.analyzeImage(base64Image, problemContext);
// Clean up uploaded file
fs.unlinkSync(req.file.path);
res.json({
success: true,
analysis: analysis.analysis,
suggestedTrades: analysis.suggestedTrades,
urgency: analysis.urgency,
urgencyReasoning: analysis.urgencyReasoning,
materialEstimate: analysis.materialEstimate,
timeEstimate: analysis.timeEstimate,
complexityLevel: analysis.complexityLevel,
safetyIssues: analysis.safetyIssues,
followUpQuestions: analysis.followUpQuestions,
confidence: analysis.confidence
});
} catch (analysisError) {
console.error('Image analysis error:', analysisError);
// Clean up uploaded file
fs.unlinkSync(req.file.path);
// Generate AI-powered fallback questions for image analysis failure
try {
const fallbackResponse = await matchingAgent.generateImageAnalysisFallback(problemContext);
res.json({
success: true,
analysis: fallbackResponse.analysis,
suggestedTrades: [],
followUpQuestions: fallbackResponse.followUpQuestions,
confidence: 0.3
});
} catch (fallbackError) {
res.json({
success: true,
analysis: 'Image uploaded successfully, but analysis is temporarily unavailable.',
suggestedTrades: [],
followUpQuestions: [],
confidence: 0.3
});
}
}
} catch (error) {
console.error('Image upload error:', error);
res.status(500).json({ error: 'Failed to process image' });
}
});
// Create uploads directory
const fs = require('fs');
if (!fs.existsSync('uploads')) {
fs.mkdirSync('uploads');
}
app.listen(PORT, () => {
console.log(`PowerUs AI server running on port ${PORT}`);
console.log(`Visit http://localhost:${PORT} to use the application`);
});