Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions backend/migrations/20250311080555_add_ai_usage_limits_to_users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Knex } from 'knex'

export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('users', table => {
// Track daily AI usage count
table.integer('ai_usage_count').defaultTo(0).notNullable()
// Store the last reset date for the AI usage count
table.date('ai_usage_reset_date').nullable()
// Store the last generated challenge for AI usage
table.string('ai_challenge_uuid', 36).nullable()
// Store the timestamp when the challenge was generated
table.timestamp('ai_challenge_created_at').nullable()
})
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('users', table => {
table.dropColumn('ai_usage_count')
table.dropColumn('ai_usage_reset_date')
table.dropColumn('ai_challenge_uuid')
table.dropColumn('ai_challenge_created_at')
})
}
295 changes: 220 additions & 75 deletions backend/src/routes/ai.ts
Original file line number Diff line number Diff line change
@@ -1,122 +1,267 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import express from 'express'
import express, { Request, Response } from 'express'
import { Knex } from 'knex'
import { AiPromptRequest, AiPromptResponse } from '../types/ai'
import { AiChallengeVerifyDTO } from '../types/ai'
import { AiPromptRequest } from '../types/ai'
import { AiService } from '../utils/ai-service'
import { AiUsageService } from '../utils/ai-usage'
import { requireAuth } from '../utils/auth'

// Define a custom Request type that includes the address property
interface CustomRequest extends Request {
address?: string
}

// Get API key from environment variables
const OPENAI_API_KEY = process.env.OPENAI_API_KEY || ''

/**
* Creates a router for AI-related endpoints
* @param db - Database connection
* @param aiServiceOverride - Optional AiService instance for testing
* Creates an Express router for AI-related endpoints
* @param db - Knex database instance
* @param customAiService - Optional custom AI service for testing
* @param customAiUsageService - Optional custom AI usage service for testing
* @returns Express router with AI endpoints
*/
export function createAiRouter(db: Knex, aiServiceOverride?: AiService): express.Router {
export function createAiRouter(
db: Knex,
customAiService: AiService | null = null,
customAiUsageService: AiUsageService | null = null,
): express.Router {
const router = express.Router()

// Create AI service if API key is available or use the provided override
let aiService: AiService | null = aiServiceOverride || null
// Create AI service if not provided
const aiService = customAiService || (OPENAI_API_KEY ? new AiService({ apiKey: OPENAI_API_KEY }) : null)

if (!aiService && OPENAI_API_KEY) {
aiService = new AiService({
apiKey: OPENAI_API_KEY,
model: 'gpt-4o-mini', // Use gpt-4o-mini model
temperature: 0.7,
maxTokens: 500,
})
}
// Create AI usage service if not provided
const aiUsageService = customAiUsageService || new AiUsageService(db)

/**
* Process a prompt with AI
* POST /api/ai/process-prompt
* Generate a challenge for AI usage
* GET /api/ai/challenge
*/
router.post('/process-prompt', async (req, res) => {
const { prompt, templateId } = req.body as AiPromptRequest
router.get('/challenge', requireAuth, async (req: CustomRequest, res: Response) => {
try {
const address = req.address

if (!address) {
return res.status(401).json({
success: false,
error: 'Authentication required',
})
}

// Validate required fields
if (!prompt || !templateId) {
return res.status(400).json({
// Generate a challenge
const challenge = await aiUsageService.generateChallenge(address)

return res.json({
success: true,
data: challenge,
})
} catch (error) {
console.error('Error generating AI challenge:', error instanceof Error ? error.message : 'Unknown error')
return res.status(500).json({
success: false,
error: 'Missing required fields: prompt and templateId are required',
error: 'Failed to generate challenge',
})
Comment on lines +61 to 65

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error message here is a bit generic. Consider providing more specific feedback to the user about why the challenge generation failed. For example, you could check if the user exists before attempting to generate the challenge and return a more informative error message if the user is not found.

      console.error('Error generating AI challenge:', error instanceof Error ? error.message : 'Unknown error')
      const errorMessage = error instanceof Error ? error.message : 'Unknown error'
      return res.status(500).json({
        success: false,
        error: `Failed to generate challenge: ${errorMessage}`, // More specific error message
      })

}
})

/**
* Verify a challenge for AI usage
* POST /api/ai/verify-challenge
*/
router.post('/verify-challenge', requireAuth, async (req: CustomRequest, res: Response) => {
try {
const { address, challenge, signature } = req.body as AiChallengeVerifyDTO

// Verify the challenge
const result = await aiUsageService.verifyChallenge(address, challenge, signature)

// Check if AI service is available
if (!aiService) {
return res.status(503).json({
return res.json({
success: result.success,
data: {
remaining_attempts: result.remaining_attempts,
max_attempts: result.max_attempts,
},
})
} catch (error) {
console.error('Error verifying AI challenge:', error instanceof Error ? error.message : 'Unknown error')
return res.status(500).json({
success: false,
error: 'AI service is not available. OPENAI_API_KEY may be missing.',
error: 'Failed to verify challenge',
})
Comment on lines +88 to 92

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the previous comment, the error message here is also generic. Consider adding more context to the error message to help the user understand why the challenge verification failed. For example, you could check if the challenge is valid before attempting to verify it and return a more informative error message if the challenge is invalid.

      console.error('Error verifying AI challenge:', error instanceof Error ? error.message : 'Unknown error')
      const errorMessage = error instanceof Error ? error.message : 'Unknown error'
      return res.status(500).json({
        success: false,
        error: `Failed to verify challenge: ${errorMessage}`, // More specific error message
      })

}
})

// Verify template exists
/**
* Get remaining AI requests for a user
* GET /api/ai/remaining-requests
*/
router.get('/remaining-requests', requireAuth, async (req: CustomRequest, res: Response) => {
try {
const template = await db('templates').where({ id: templateId }).first()
const address = req.address

if (!template) {
return res.status(404).json({
if (!address) {
return res.status(401).json({
success: false,
error: 'Template not found',
error: 'Authentication required',
})
}

if (!template.json_data) {
return res.status(400).json({
// Get remaining requests
const remaining = await aiUsageService.getRemainingRequests(address)

return res.json({
success: true,
data: remaining,
})
} catch (error) {
console.error('Error getting remaining AI requests:', error instanceof Error ? error.message : 'Unknown error')
return res.status(500).json({
success: false,
error: 'Failed to get remaining requests',
})
Comment on lines +119 to +123

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error message here is also generic. Consider adding more context to the error message to help the user understand why getting the remaining AI requests failed. For example, you could check if the user exists before attempting to get the remaining requests and return a more informative error message if the user is not found.

      console.error('Error getting remaining AI requests:', error instanceof Error ? error.message : 'Unknown error')
      const errorMessage = error instanceof Error ? error.message : 'Unknown error'
      return res.status(500).json({
        success: false,
        error: `Failed to get remaining requests: ${errorMessage}`, // More specific error message
      })

}
})

/**
* Process a prompt with a template
* POST /api/ai/process-prompt
*/
router.post(
'/process-prompt',
async (req: Request, res: Response, next: () => void) => {
// Check if AI service is available first
if (!aiService) {
return res.status(503).json({
success: false,
error: 'Template JSON data is missing',
error: 'AI service is not available',
})
}

// Log the received request
console.log(`Processing prompt against template ${templateId}`)
// If AI service is available, proceed with authentication
requireAuth(req as CustomRequest, res, next)
},
async (req: CustomRequest, res: Response) => {
try {
const { prompt, templateId, challenge, signature } = req.body as AiPromptRequest
const address = req.address

if (!address) {
return res.status(401).json({
success: false,
error: 'Authentication required',
})
}

// Validate required fields
if (!prompt || !templateId || !challenge || !signature) {
return res.status(400).json({
success: false,
error: 'Missing required fields',
})
}

// Verify the challenge
try {
const verificationResult = await aiUsageService.verifyChallenge(address, challenge, signature)

// If verification failed due to usage limits
if (!verificationResult.success) {
return res.status(403).json({
success: false,
error: 'Daily AI usage limit reached',
data: {
remaining_attempts: verificationResult.remaining_attempts,
max_attempts: verificationResult.max_attempts,
},
})
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error('Error verifying challenge:', errorMessage)
return res.status(403).json({
success: false,
error: 'Challenge verification failed',
})
Comment on lines +183 to +186

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Returning a 403 status code here might not be the most appropriate choice. A 403 status code typically indicates that the server understands the request but refuses to authorize it. In this case, the user is authorized, but they have exceeded their daily usage limit. A more appropriate status code might be 429 (Too Many Requests).

          return res.status(429).json({
            success: false,
            error: 'Daily AI usage limit reached',
            data: {
              remaining_attempts: verificationResult.remaining_attempts,
              max_attempts: verificationResult.max_attempts,
            },
          })

}

// Get the template
const template = await db('templates').where({ id: templateId }).first()
if (!template) {
return res.status(404).json({
success: false,
error: 'Template not found',
})
}

// Default system prompt for JSON generation
const defaultSystemPrompt = `You are a specialized JSON generator. Your task is to create a valid JSON object that matches the provided schema based on the user's request.`
// Check if template has JSON data
if (template.json_data === '') {
console.error('Database error: Empty JSON data')
return res.status(500).json({
success: false,
error: 'Internal server error',
})
}

// Build enhanced system prompt with template context
const enhancedSystemPrompt = `
${defaultSystemPrompt}
if (!template.json_data || template.json_data === '{}') {
return res.status(400).json({
success: false,
error: 'Template JSON data is missing',
})
}

This template requires generating JSON that strictly follows this schema specification:
`
// Parse the template JSON data
let schema: Record<string, unknown>
let systemPrompt: string
try {
const jsonData = JSON.parse(template.json_data)
if (typeof jsonData !== 'object' || !jsonData || !jsonData.schema || typeof jsonData.schema !== 'object') {
return res.status(400).json({
success: false,
error: 'Invalid template JSON data: missing or invalid schema',
})
}
schema = jsonData.schema as Record<string, unknown>
systemPrompt =
typeof jsonData.systemPrompt === 'string' ? jsonData.systemPrompt : 'You are a helpful assistant.'
} catch (error) {
// Invalid JSON format
console.error('Error parsing template JSON data:', error instanceof Error ? error.message : 'Unknown error')
return res.status(400).json({
success: false,
error: 'Invalid template JSON data',
})
}

// Process the prompt with AI
const aiResponse = await aiService.processTemplatePrompt(prompt, template.json_data, enhancedSystemPrompt)
// Check if AI service is available
if (!aiService) {
return res.status(503).json({
success: false,
error: 'AI service is not available',
})
}

// Check if the response is valid JSON
if (!aiResponse.isValid) {
// Fallback response when JSON parsing fails
return res.status(200).json({
// Process the prompt with the template
const result = await aiService.processTemplatePrompt(prompt, schema, systemPrompt)

// Return the response
return res.json({
success: true,
data: {
result: {
rawText: aiResponse.rawResponse,
message: 'AI response could not be parsed as valid JSON.',
timestamp: new Date().toISOString(),
},
requiredValidation: true,
result: result.parsedData || {},
requiredValidation: !result.isValid,
},
} as AiPromptResponse)
})
} catch (error) {
console.error('Error processing prompt:', error instanceof Error ? error.message : 'Unknown error')
return res.status(500).json({
success: false,
error: 'Internal server error',
})
}

// Return the successful response
return res.status(200).json({
success: true,
data: {
result: aiResponse.parsedData || {},
requiredValidation: false,
},
} as AiPromptResponse)
} catch (error) {
console.error('Error processing AI prompt:', error)
return res.status(500).json({
success: false,
error: 'Internal server error',
})
}
})
},
)

return router
}
Loading
Loading