/** * AI Service for generating intelligent task suggestions * Uses OpenAI API to analyze street conditions and suggest maintenance tasks */ const axios = require("axios"); const logger = require("../utils/logger"); class AIService { constructor() { this.apiKey = process.env.OPENAI_API_KEY; this.model = process.env.OPENAI_MODEL || "gpt-3.5-turbo"; this.apiUrl = "https://api.openai.com/v1/chat/completions"; this.isConfigured = !!this.apiKey; if (!this.isConfigured) { logger.warn("OpenAI API key not configured. AI service will return mock data."); } else { logger.info("AI Service initialized", { model: this.model }); } } /** * Generate task suggestions for a street * @param {Object} options - Options for generating suggestions * @param {string} options.streetName - Name of the street * @param {Object} options.location - Location coordinates {type: 'Point', coordinates: [lng, lat]} * @param {Array} options.pastTasks - Array of past task descriptions * @param {Object} options.weather - Weather conditions (optional) * @param {Array} options.communityPriorities - Community priorities (optional) * @param {number} options.numberOfSuggestions - Number of suggestions to generate (default: 5) * @returns {Promise} Array of task suggestions */ async generateTaskSuggestions(options = {}) { const { streetName, location, pastTasks = [], weather = null, communityPriorities = [], numberOfSuggestions = 5 } = options; // If API is not configured, return mock suggestions with warning if (!this.isConfigured) { logger.warn("Returning mock suggestions - OpenAI API not configured"); return this._getMockSuggestions(streetName, numberOfSuggestions); } try { // Build context-aware prompt const prompt = this._buildPrompt({ streetName, location, pastTasks, weather, communityPriorities, numberOfSuggestions }); logger.debug("Generating AI task suggestions", { streetName, model: this.model, pastTasksCount: pastTasks.length }); // Call OpenAI API const response = await axios.post( this.apiUrl, { model: this.model, messages: [ { role: "system", content: "You are a helpful assistant that generates practical street maintenance task suggestions for community volunteers. Focus on actionable, safe, and impactful tasks that can be completed by regular citizens." }, { role: "user", content: prompt } ], temperature: 0.7, max_tokens: 1000 }, { headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.apiKey}` }, timeout: 10000 // 10 second timeout } ); // Parse the response const suggestions = this._parseOpenAIResponse(response.data, streetName); logger.info("AI task suggestions generated successfully", { streetName, count: suggestions.length }); return suggestions; } catch (error) { logger.error("Error generating AI task suggestions", error, { streetName, errorType: error.response?.status || error.code }); // Fallback to mock suggestions on error logger.info("Falling back to mock suggestions due to API error"); return this._getMockSuggestions(streetName, numberOfSuggestions); } } /** * Build a context-aware prompt for OpenAI * @private */ _buildPrompt(options) { const { streetName, location, pastTasks, weather, communityPriorities, numberOfSuggestions } = options; let prompt = `Generate ${numberOfSuggestions} practical street maintenance task suggestions for "${streetName}".`; // Add location context if available if (location && location.coordinates) { const [lng, lat] = location.coordinates; prompt += `\nLocation: Latitude ${lat.toFixed(4)}, Longitude ${lng.toFixed(4)}`; } // Add past tasks context if (pastTasks.length > 0) { prompt += `\n\nRecent tasks completed on this street:`; pastTasks.slice(0, 10).forEach((task, index) => { prompt += `\n${index + 1}. ${task}`; }); prompt += `\n\nAvoid suggesting tasks that are too similar to recently completed ones.`; } // Add weather context if (weather) { prompt += `\n\nCurrent weather conditions: ${weather.description || weather.condition}`; if (weather.temperature) { prompt += ` (${weather.temperature}°F)`; } prompt += `\nSuggest tasks appropriate for these weather conditions.`; } // Add community priorities if (communityPriorities.length > 0) { prompt += `\n\nCommunity priorities: ${communityPriorities.join(", ")}`; prompt += `\nPrioritize suggestions that align with these community goals.`; } // Add formatting instructions prompt += `\n\nFormat your response as a JSON array of objects, each with: - "description": A clear, actionable task description (2-8 words) - "details": A brief explanation of why this task is important (1 sentence) - "estimatedTime": Estimated time to complete in minutes (number) - "difficulty": Either "easy", "moderate", or "hard" - "priority": Either "low", "medium", or "high" Example format: [ { "description": "Remove litter from sidewalk", "details": "Improves neighborhood appearance and prevents storm drain blockage.", "estimatedTime": 30, "difficulty": "easy", "priority": "medium" } ] Ensure tasks are safe for volunteers without specialized equipment.`; return prompt; } /** * Parse OpenAI API response into task suggestions * @private */ _parseOpenAIResponse(data, streetName) { try { const content = data.choices[0]?.message?.content; if (!content) { throw new Error("No content in OpenAI response"); } // Extract JSON from response (handle markdown code blocks) let jsonStr = content.trim(); // Remove markdown code block syntax if present if (jsonStr.startsWith("```")) { jsonStr = jsonStr.replace(/```json?\n?/g, "").replace(/```\n?/g, ""); } const suggestions = JSON.parse(jsonStr); // Validate and format suggestions if (!Array.isArray(suggestions)) { throw new Error("Response is not an array"); } return suggestions.map(suggestion => ({ street: streetName, description: suggestion.description || "Maintenance task", details: suggestion.details || "", estimatedTime: suggestion.estimatedTime || 60, difficulty: suggestion.difficulty || "moderate", priority: suggestion.priority || "medium", source: "ai" })); } catch (error) { logger.error("Error parsing OpenAI response", error, { response: data.choices[0]?.message?.content?.substring(0, 200) }); throw new Error("Failed to parse AI response"); } } /** * Get mock task suggestions as fallback * @private */ _getMockSuggestions(streetName, count = 5) { const mockPool = [ { description: "Remove litter from sidewalk", details: "Improves neighborhood appearance and prevents storm drain blockage.", estimatedTime: 30, difficulty: "easy", priority: "medium" }, { description: "Clear leaves from gutters", details: "Prevents water accumulation and potential flooding during rain.", estimatedTime: 45, difficulty: "easy", priority: "high" }, { description: "Trim overgrown vegetation", details: "Maintains clear walkways and improves visibility for pedestrians.", estimatedTime: 60, difficulty: "moderate", priority: "medium" }, { description: "Paint over graffiti", details: "Restores aesthetic appeal and discourages vandalism.", estimatedTime: 90, difficulty: "moderate", priority: "high" }, { description: "Report broken street lights", details: "Enhances safety and security for residents during nighttime.", estimatedTime: 15, difficulty: "easy", priority: "high" }, { description: "Clean storm drains", details: "Ensures proper drainage and prevents flooding.", estimatedTime: 40, difficulty: "moderate", priority: "medium" }, { description: "Repair minor sidewalk cracks", details: "Prevents trip hazards and further deterioration.", estimatedTime: 120, difficulty: "hard", priority: "low" }, { description: "Water street trees", details: "Maintains urban canopy and provides shade for the community.", estimatedTime: 25, difficulty: "easy", priority: "low" } ]; // Randomly select suggestions from the pool const shuffled = mockPool.sort(() => 0.5 - Math.random()); const selected = shuffled.slice(0, Math.min(count, mockPool.length)); return selected.map(suggestion => ({ street: streetName || "Selected Street", ...suggestion, source: "mock" })); } /** * Check if AI service is properly configured * @returns {boolean} */ isAvailable() { return this.isConfigured; } /** * Get service status information * @returns {Object} */ getStatus() { return { configured: this.isConfigured, model: this.isConfigured ? this.model : null, status: this.isConfigured ? "active" : "mock_mode" }; } } // Export singleton instance module.exports = new AIService();