feat: integrate OpenAI API for intelligent task suggestions

Implement real OpenAI integration for the AI task suggestions feature:

- Create aiService.js with GPT-3.5/GPT-4 integration
- Add context-aware prompt engineering (street data, past tasks, weather, priorities)
- Implement automatic fallback to high-quality mock suggestions
- Add graceful degradation when API key not configured
- Create comprehensive error handling and timeout protection
- Add input validation with aiValidator middleware
- Implement /api/ai/status endpoint for service monitoring
- Add 12 passing test cases covering all functionality
- Document OpenAI model configuration in .env.example
- Create detailed AI_SERVICE.md documentation

Key features:
- Uses axios to call OpenAI API directly (no SDK dependency)
- Analyzes street conditions and past 30 days of completed tasks
- Generates structured JSON responses with task metadata
- 10-second timeout with automatic fallback
- Comprehensive logging using centralized logger service
- Returns warnings when running in mock mode

All tests pass (12/12). Ready for production use with or without API key.

🤖 Generated with AI Assistant

Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
This commit is contained in:
William Valentin
2025-11-03 13:19:00 -08:00
parent a12519aa41
commit 771d39a52b
6 changed files with 1005 additions and 12 deletions

View File

@@ -28,4 +28,7 @@ STRIPE_SECRET_KEY=your_stripe_secret_key
STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key
# OpenAI Configuration (optional - for AI features)
# If not set, AI features will return mock data
OPENAI_API_KEY=your_openai_api_key
OPENAI_MODEL=gpt-3.5-turbo
# Available models: gpt-3.5-turbo, gpt-4, gpt-4-turbo-preview

265
backend/AI_SERVICE.md Normal file
View File

@@ -0,0 +1,265 @@
# AI Service Documentation
## Overview
The AI Service provides intelligent task suggestions for street maintenance using OpenAI's GPT models. It analyzes street conditions, historical data, weather patterns, and community priorities to generate contextually relevant and actionable maintenance tasks.
## Features
- **Context-Aware Suggestions**: Generates tasks based on street name, location, and past maintenance history
- **Weather Integration**: Considers current weather conditions for appropriate task suggestions
- **Community Priorities**: Aligns suggestions with community goals and priorities
- **Fallback Mode**: Automatically falls back to high-quality mock suggestions if OpenAI API is unavailable
- **Graceful Degradation**: Works without API key configuration, providing mock data with warnings
## Configuration
### Environment Variables
```env
# Required for AI features
OPENAI_API_KEY=sk-your-api-key-here
# Optional - defaults to gpt-3.5-turbo
OPENAI_MODEL=gpt-3.5-turbo
```
**Available Models:**
- `gpt-3.5-turbo` (default) - Fast, cost-effective, good for most use cases
- `gpt-4` - More advanced reasoning, higher quality suggestions
- `gpt-4-turbo-preview` - Latest GPT-4 with improved performance
### Cost Considerations
- **gpt-3.5-turbo**: ~$0.001-0.002 per request (typical task suggestion)
- **gpt-4**: ~$0.03-0.06 per request
- Each task suggestion request typically uses 500-1000 tokens
## API Endpoints
### Check AI Service Status
**GET** `/api/ai/status`
Returns the current status of the AI service.
**Headers:**
```
x-auth-token: <jwt-token>
```
**Response:**
```json
{
"success": true,
"data": {
"configured": true,
"model": "gpt-3.5-turbo",
"status": "active"
}
}
```
### Get Task Suggestions
**GET** `/api/ai/task-suggestions`
Generate AI-powered task suggestions for street maintenance.
**Headers:**
```
x-auth-token: <jwt-token>
```
**Query Parameters:**
- `streetId` (optional, string): ID of the specific street to get suggestions for
- `count` (optional, integer): Number of suggestions to generate (1-10, default: 5)
**Response:**
```json
{
"success": true,
"data": [
{
"street": "Main Street",
"description": "Remove litter from sidewalk",
"details": "Improves neighborhood appearance and prevents storm drain blockage.",
"estimatedTime": 30,
"difficulty": "easy",
"priority": "medium",
"source": "ai"
}
],
"meta": {
"source": "ai",
"count": 5,
"aiConfigured": true
}
}
```
**Response (without API key):**
```json
{
"success": true,
"data": [...],
"meta": {
"source": "mock",
"count": 5,
"aiConfigured": false
},
"warning": "OpenAI API not configured. Returning mock suggestions. Set OPENAI_API_KEY in environment variables for AI-powered suggestions."
}
```
## Service Usage
### Import the Service
```javascript
const aiService = require('./services/aiService');
```
### Generate Task Suggestions
```javascript
const suggestions = await aiService.generateTaskSuggestions({
streetName: "Main Street",
location: {
type: "Point",
coordinates: [-122.4194, 37.7749]
},
pastTasks: [
"Remove graffiti from wall",
"Clear storm drains"
],
weather: {
condition: "Partly cloudy",
description: "Clear with mild temperatures",
temperature: 72
},
communityPriorities: [
"Safety improvements",
"Environmental sustainability"
],
numberOfSuggestions: 5
});
```
### Check Service Availability
```javascript
if (aiService.isAvailable()) {
// Use AI-powered suggestions
} else {
// Use mock suggestions or alternative logic
}
```
### Get Service Status
```javascript
const status = aiService.getStatus();
// Returns: { configured: boolean, model: string, status: string }
```
## Suggestion Structure
Each task suggestion includes:
- **street** (string): Name of the street
- **description** (string): Brief, actionable task description (2-8 words)
- **details** (string): Explanation of why the task is important (1 sentence)
- **estimatedTime** (number): Estimated completion time in minutes
- **difficulty** (string): Task difficulty - "easy", "moderate", or "hard"
- **priority** (string): Task priority - "low", "medium", or "high"
- **source** (string): Source of suggestion - "ai" or "mock"
## Prompt Engineering
The AI service uses carefully crafted prompts to generate high-quality suggestions:
1. **Context Building**: Includes street name, location, and relevant historical data
2. **Past Tasks Analysis**: Avoids suggesting recently completed tasks
3. **Weather Awareness**: Suggests weather-appropriate activities
4. **Community Alignment**: Prioritizes tasks aligned with community goals
5. **Safety Focus**: Emphasizes tasks safe for volunteers without specialized equipment
6. **Structured Output**: Requests JSON format with specific fields
## Error Handling
The service implements comprehensive error handling:
1. **API Failures**: Automatically falls back to mock suggestions
2. **Timeout Protection**: 10-second timeout on API requests
3. **Response Parsing**: Handles various response formats (including markdown code blocks)
4. **Validation**: Ensures all suggestions have required fields
5. **Logging**: Detailed error logging for debugging
## Testing
Run the AI service tests:
```bash
npm test -- __tests__/routes/ai.test.js
```
Test coverage includes:
- Service status endpoint
- Task suggestion generation (with and without street ID)
- Input validation
- Mock mode functionality
- Error handling and fallbacks
- Service integration tests
## Best Practices
1. **Cache Suggestions**: Consider caching suggestions for frequently requested streets
2. **Rate Limiting**: Implement rate limiting to control API costs
3. **Batch Processing**: Generate suggestions for multiple streets in batches when possible
4. **Monitor Usage**: Track OpenAI API usage and costs
5. **Fallback Strategy**: Always have mock suggestions available as fallback
6. **User Feedback**: Collect feedback on suggestion quality to improve prompts
## Troubleshooting
### Issue: "OpenAI API not configured" warning
**Solution**: Set `OPENAI_API_KEY` environment variable with your OpenAI API key.
### Issue: Suggestions are not contextual
**Solution**: Ensure you're passing street details, location, and past tasks to the service.
### Issue: API timeout errors
**Solution**: Check network connectivity to OpenAI API. The service will automatically fall back to mock suggestions.
### Issue: High API costs
**Solution**:
- Use `gpt-3.5-turbo` instead of `gpt-4`
- Implement caching for frequently requested streets
- Add rate limiting to the endpoint
- Reduce `numberOfSuggestions` parameter
## Future Enhancements
Potential improvements to the AI service:
- [ ] Integration with real-time weather APIs
- [ ] Machine learning on task completion rates
- [ ] Seasonal task suggestions
- [ ] User preference learning
- [ ] Image analysis for street condition assessment
- [ ] Multi-language support
- [ ] Integration with city planning APIs
- [ ] Automated task prioritization based on urgency
## Support
For issues or questions:
- Check logs at `backend/server.log`
- Review error messages in the response
- Ensure environment variables are correctly set
- Verify OpenAI API key is valid and has sufficient credits

View File

@@ -0,0 +1,255 @@
const request = require("supertest");
const express = require("express");
// Mock CouchDB service before importing routes
jest.mock("../../services/couchdbService", () => ({
initialize: jest.fn().mockResolvedValue(true),
create: jest.fn(),
getById: jest.fn(),
find: jest.fn(),
createDocument: jest.fn(),
updateDocument: jest.fn(),
deleteDocument: jest.fn(),
getDocument: jest.fn(),
}));
// Mock logger to avoid console output during tests
jest.mock("../../utils/logger", () => ({
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
}));
// Mock auth middleware
jest.mock("../../middleware/auth", () => (req, res, next) => {
req.user = { id: "test-user-id" };
next();
});
const aiRoute = require("../../routes/ai");
const aiService = require("../../services/aiService");
const Street = require("../../models/Street");
const Task = require("../../models/Task");
const couchdbService = require("../../services/couchdbService");
// Setup Express app for testing
const app = express();
app.use(express.json());
app.use("/api/ai", aiRoute);
describe("AI Routes", () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe("GET /api/ai/status", () => {
it("should return AI service status", async () => {
const response = await request(app)
.get("/api/ai/status")
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty("configured");
expect(response.body.data).toHaveProperty("status");
});
});
describe("GET /api/ai/task-suggestions", () => {
it("should return task suggestions without streetId", async () => {
const response = await request(app)
.get("/api/ai/task-suggestions")
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toBeInstanceOf(Array);
expect(response.body.data.length).toBeGreaterThan(0);
expect(response.body.meta).toHaveProperty("source");
expect(response.body.meta).toHaveProperty("aiConfigured");
// Check suggestion structure
const suggestion = response.body.data[0];
expect(suggestion).toHaveProperty("description");
expect(suggestion).toHaveProperty("details");
expect(suggestion).toHaveProperty("estimatedTime");
expect(suggestion).toHaveProperty("difficulty");
expect(suggestion).toHaveProperty("priority");
expect(suggestion).toHaveProperty("source");
});
it("should return task suggestions with custom count", async () => {
const response = await request(app)
.get("/api/ai/task-suggestions?count=3")
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toBeInstanceOf(Array);
expect(response.body.data.length).toBe(3);
});
it("should validate count parameter maximum", async () => {
const response = await request(app)
.get("/api/ai/task-suggestions?count=15")
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.errors).toBeInstanceOf(Array);
});
it("should validate count is a positive number", async () => {
const response = await request(app)
.get("/api/ai/task-suggestions?count=0")
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.errors).toBeInstanceOf(Array);
});
it("should return task suggestions for specific street", async () => {
const streetId = "test-street-123";
const streetData = {
_id: streetId,
_rev: "1-abc",
type: "street",
name: "Test Avenue",
location: {
type: "Point",
coordinates: [-122.4194, 37.7749]
},
status: "available",
stats: {
completedTasksCount: 0,
reportsCount: 0,
openReportsCount: 0
}
};
// Mock street lookup
couchdbService.getDocument.mockResolvedValueOnce(streetData);
// Mock task lookup
couchdbService.find.mockResolvedValueOnce([
{
_id: "task-1",
type: "task",
street: { streetId, name: "Test Avenue" },
description: "Remove graffiti from wall",
status: "completed",
completedAt: new Date().toISOString()
},
{
_id: "task-2",
type: "task",
street: { streetId, name: "Test Avenue" },
description: "Clear storm drains",
status: "completed",
completedAt: new Date().toISOString()
}
]);
const response = await request(app)
.get(`/api/ai/task-suggestions?streetId=${streetId}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toBeInstanceOf(Array);
expect(response.body.data.length).toBeGreaterThan(0);
// Check that street name is included
const suggestion = response.body.data[0];
expect(suggestion.street).toBe("Test Avenue");
});
it("should return 404 for non-existent street", async () => {
// Mock street not found
couchdbService.getDocument.mockResolvedValueOnce(null);
const response = await request(app)
.get("/api/ai/task-suggestions?streetId=nonexistent-id")
.expect(404);
expect(response.body.success).toBe(false);
expect(response.body.msg).toBe("Street not found");
});
it("should include warning when OpenAI is not configured", async () => {
const response = await request(app)
.get("/api/ai/task-suggestions")
.expect(200);
expect(response.body.success).toBe(true);
if (!aiService.isAvailable()) {
expect(response.body.warning).toBeDefined();
expect(response.body.warning).toContain("OpenAI API not configured");
}
});
});
describe("AI Service Integration", () => {
it("should generate suggestions with proper structure", async () => {
const suggestions = await aiService.generateTaskSuggestions({
streetName: "Main Street",
numberOfSuggestions: 5
});
expect(suggestions).toBeInstanceOf(Array);
expect(suggestions.length).toBe(5);
suggestions.forEach(suggestion => {
expect(suggestion).toHaveProperty("street");
expect(suggestion).toHaveProperty("description");
expect(suggestion).toHaveProperty("details");
expect(suggestion).toHaveProperty("estimatedTime");
expect(suggestion).toHaveProperty("difficulty");
expect(suggestion).toHaveProperty("priority");
expect(suggestion).toHaveProperty("source");
expect(["easy", "moderate", "hard"]).toContain(suggestion.difficulty);
expect(["low", "medium", "high"]).toContain(suggestion.priority);
});
});
it("should respect numberOfSuggestions parameter", async () => {
const suggestions1 = await aiService.generateTaskSuggestions({
streetName: "Oak Avenue",
numberOfSuggestions: 3
});
const suggestions2 = await aiService.generateTaskSuggestions({
streetName: "Elm Street",
numberOfSuggestions: 7
});
expect(suggestions1.length).toBe(3);
expect(suggestions2.length).toBe(7);
});
it("should include street name in suggestions", async () => {
const streetName = "Custom Street Name";
const suggestions = await aiService.generateTaskSuggestions({
streetName,
numberOfSuggestions: 3
});
suggestions.forEach(suggestion => {
expect(suggestion.street).toBe(streetName);
});
});
it("should return mock data when OpenAI is not configured", async () => {
const suggestions = await aiService.generateTaskSuggestions({
streetName: "Test Street",
numberOfSuggestions: 5
});
expect(suggestions).toBeInstanceOf(Array);
expect(suggestions.length).toBe(5);
// Mock suggestions should have source: "mock"
if (!aiService.isAvailable()) {
expect(suggestions[0].source).toBe("mock");
}
});
});
});

View File

@@ -0,0 +1,35 @@
const { query, validationResult } = require("express-validator");
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array().map((err) => ({
field: err.path,
message: err.msg,
})),
});
}
next();
};
/**
* Task suggestions validation
*/
const taskSuggestionsValidation = [
query("streetId")
.optional()
.isString()
.withMessage("Street ID must be a string"),
query("count")
.optional()
.isInt({ min: 1, max: 10 })
.withMessage("Count must be between 1 and 10"),
validate,
];
module.exports = {
taskSuggestionsValidation,
validate,
};

View File

@@ -1,22 +1,129 @@
const express = require("express");
const auth = require("../middleware/auth");
const aiService = require("../services/aiService");
const logger = require("../utils/logger");
const { taskSuggestionsValidation } = require("../middleware/validators/aiValidator");
const Street = require("../models/Street");
const Task = require("../models/Task");
const router = express.Router();
// Get AI task suggestions
router.get("/task-suggestions", auth, async (req, res) => {
// Get AI service status
router.get("/status", auth, async (req, res) => {
try {
// In a real application, you would use a more sophisticated AI model to generate task suggestions.
// For this example, we'll just return some mock data.
const suggestions = [
{ street: "Main St", description: "Clean up litter" },
{ street: "Elm St", description: "Remove graffiti" },
{ street: "Oak Ave", description: "Mow the lawn" },
];
res.json(suggestions);
const status = aiService.getStatus();
res.json({
success: true,
data: status
});
} catch (err) {
console.error(err.message);
res.status(500).send("Server error");
logger.error("Error getting AI service status", err);
res.status(500).json({
success: false,
msg: "Server error"
});
}
});
// Get AI task suggestions
router.get("/task-suggestions", auth, taskSuggestionsValidation, async (req, res) => {
try {
const { streetId, count = 5 } = req.query;
const numberOfSuggestions = parseInt(count, 10);
let streetName = "General Street";
let location = null;
let pastTasks = [];
// If streetId is provided, fetch street details and past tasks
if (streetId) {
const street = await Street.findById(streetId);
if (!street) {
return res.status(404).json({
success: false,
msg: "Street not found"
});
}
streetName = street.name;
location = street.location;
// Fetch past completed tasks for this street
const tasks = await Task.find({
"street.streetId": streetId,
status: "completed"
});
// Extract task descriptions from past 30 days
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
pastTasks = tasks
.filter(task => new Date(task.completedAt) > thirtyDaysAgo)
.map(task => task.description)
.slice(0, 10); // Limit to 10 most recent tasks
logger.info("Fetching AI suggestions for specific street", {
streetId,
streetName,
pastTasksCount: pastTasks.length
});
} else {
logger.info("Fetching general AI suggestions");
}
// Mock weather data (in production, this could call a weather API)
const weather = {
condition: "Partly cloudy",
description: "Clear with mild temperatures",
temperature: 72
};
// Example community priorities (in production, these could come from database)
const communityPriorities = [
"Safety improvements",
"Environmental sustainability",
"Neighborhood beautification"
];
// Generate suggestions using AI service
const suggestions = await aiService.generateTaskSuggestions({
streetName,
location,
pastTasks,
weather,
communityPriorities,
numberOfSuggestions
});
// Add warning if using mock data
const response = {
success: true,
data: suggestions,
meta: {
source: suggestions[0]?.source || "unknown",
count: suggestions.length,
aiConfigured: aiService.isAvailable()
}
};
if (!aiService.isAvailable()) {
response.warning = "OpenAI API not configured. Returning mock suggestions. Set OPENAI_API_KEY in environment variables for AI-powered suggestions.";
}
res.json(response);
} catch (err) {
logger.error("Error generating task suggestions", err, {
streetId: req.query.streetId,
userId: req.user.id
});
res.status(500).json({
success: false,
msg: "Server error while generating task suggestions"
});
}
});

View File

@@ -0,0 +1,328 @@
/**
* 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>} 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();