import { Request, Response } from 'express'; import { AppDataSource } from '../config/database'; import { Agent, AgentStatus } from '../entities/Agent'; import { User } from '../entities/User'; import { IpfsService } from '../services/ipfsService'; const agentRepository = AppDataSource.getRepository(Agent); const ipfsService = new IpfsService(); interface MulterFile { fieldname: string; originalname: string; encoding: string; mimetype: string; size: number; buffer: Buffer; } export class AgentController { /** * List all approved agents (marketplace) */ async listAgents(req: Request, res: Response) { try { const { page = 1, limit = 20, search, sortBy = 'createdAt' } = req.query; const query = agentRepository .createQueryBuilder('agent') .where('agent.status = :status', { status: AgentStatus.APPROVED }); if (search) { query.andWhere( '(agent.name ILIKE :search OR agent.description ILIKE :search OR agent.category ILIKE :search)', { search: `%${search}%` } ); } // Filter by category if provided if (req.query.category) { query.andWhere('agent.category = :category', { category: req.query.category }); } const sortOrder = req.query.sortOrder === 'asc' ? 'ASC' : 'DESC'; query.orderBy(`agent.${sortBy}`, sortOrder); const skip = (Number(page) - 1) * Number(limit); query.skip(skip).take(Number(limit)); const [agents, total] = await query.getManyAndCount(); // Format agents to match expected structure const formattedAgents = agents.map(agent => ({ ...agent, avatar: agent.avatar, reputation: agent.reputation || 0, capabilities: agent.capabilities || [], // Hide endpoint from public listings endpoint: undefined, })); res.json({ agents: formattedAgents, pagination: { page: Number(page), limit: Number(limit), total, totalPages: Math.ceil(total / Number(limit)), }, }); } catch (error) { console.error('List agents error:', error); res.status(500).json({ error: 'Failed to list agents' }); } } /** * Get single agent by ID */ async getAgent(req: Request, res: Response) { try { const { id } = req.params; const agent = await agentRepository.findOne({ where: { id }, }); if (!agent) { return res.status(404).json({ error: 'Agent not found' }); } // Only show endpoint to agent creator or admins const userId = (req as any).user?.id; if (agent.creatorId !== userId && !(req as any).user?.isAdmin) { // Create a copy without the endpoint for security const { endpoint, ...agentWithoutEndpoint } = agent; res.json({ ...agentWithoutEndpoint, // Format response to match expected structure avatar: agentWithoutEndpoint.avatar, reputation: agent.reputation || 0, capabilities: agent.capabilities || [], }); return; } // Format response to match expected structure res.json({ ...agent, reputation: agent.reputation || 0, capabilities: agent.capabilities || [], }); } catch (error) { console.error('Get agent error:', error); res.status(500).json({ error: 'Failed to get agent' }); } } /** * Create new agent listing (creator) */ async createAgent(req: Request, res: Response) { try { const userId = (req as any).user.id; const isAdmin = (req as any).user.isAdmin; // Check if user is a creator or admin const userRepository = AppDataSource.getRepository(User); const user = await userRepository.findOne({ where: { id: userId } }); if (!user) { return res.status(404).json({ error: 'User not found' }); } if (!user.isCreator && !isAdmin) { return res.status(403).json({ error: 'You must be a creator to create agents', message: 'Please become a creator first by calling POST /api/users/become-creator', becomeCreatorEndpoint: '/api/users/become-creator', }); } const avatarFile = (req as any).file as MulterFile | undefined; const { name, description, endpoint, category, capabilities, price, pointsPerTask, // Required: Points charged per task (1 point = $0.05) isSubscription = false, // Default to pay-per-task subscriptionDuration, promptTemplate, metadata, } = req.body; // Parse capabilities if it's a string let parsedCapabilities: string[] = []; if (capabilities) { if (typeof capabilities === 'string') { try { parsedCapabilities = JSON.parse(capabilities); } catch { parsedCapabilities = [capabilities]; } } else if (Array.isArray(capabilities)) { parsedCapabilities = capabilities; } } // Validate required fields if (!name || !description || !endpoint || pointsPerTask === undefined) { return res.status(400).json({ error: 'Missing required fields. Name, description, endpoint, and pointsPerTask are required.', note: 'pointsPerTask is the number of points charged per task (1 point = $0.05)' }); } // Validate pointsPerTask if (pointsPerTask < 0) { return res.status(400).json({ error: 'pointsPerTask must be 0 or greater (0 = free agent)' }); } // Validate capabilities is an array if (!Array.isArray(parsedCapabilities)) { return res.status(400).json({ error: 'Capabilities must be an array' }); } // Upload avatar to IPFS if provided let avatarUrl: string | undefined; if (avatarFile) { try { const fileObj = new File( [avatarFile.buffer], avatarFile.originalname || `avatar-${Date.now()}.${avatarFile.mimetype.split('/')[1]}`, { type: avatarFile.mimetype } ); const upload = await ipfsService.uploadFile(fileObj); avatarUrl = ipfsService.getGatewayUrl(upload.cid); } catch (error) { console.error('Avatar upload error:', error); return res.status(500).json({ error: 'Failed to upload avatar image' }); } } // Upload metadata to IPFS let ipfsHash: string | undefined; try { ipfsHash = await ipfsService.uploadMetadata({ name, description, endpoint, price, creatorId: userId, category, capabilities: parsedCapabilities, metadata, }); } catch (error) { console.warn('IPFS upload failed, continuing without IPFS hash'); } // Use price as fallback for pointsPerTask if not provided (backward compatibility) const finalPointsPerTask = pointsPerTask !== undefined ? pointsPerTask : (price ? price * 20 : 0); // 1 dollar = 20 points const agent = agentRepository.create({ name, avatar: avatarUrl, description, endpoint, category, capabilities: parsedCapabilities, price: price || pointsPerTask * 0.05, // Convert points to dollars for legacy field pointsPerTask: finalPointsPerTask, isSubscription: false, // All agents now use pay-per-task model subscriptionDuration: undefined, // Not used in pay-per-task model promptTemplate, metadata: metadata ? (typeof metadata === 'string' ? JSON.parse(metadata) : metadata) : undefined, creatorId: userId, ipfsHash, status: AgentStatus.PENDING, reputation: 0, // Start at 0, will be updated based on ratings ratingCount: 0, }); const savedAgent = await agentRepository.save(agent); res.status(201).json(savedAgent); } catch (error: any) { console.error('Create agent error:', error); res.status(500).json({ error: error.message || 'Failed to create agent' }); } } /** * Update agent (creator only) */ async updateAgent(req: Request, res: Response) { try { const { id } = req.params; const userId = (req as any).user.id; const isAdmin = (req as any).user.isAdmin; const agent = await agentRepository.findOne({ where: { id }, }); if (!agent) { return res.status(404).json({ error: 'Agent not found' }); } // Only creator or admin can update if (agent.creatorId !== userId && !isAdmin) { return res.status(403).json({ error: 'Unauthorized' }); } // Can't update if approved (requires re-submission) if (agent.status === AgentStatus.APPROVED && agent.creatorId !== userId) { return res.status(400).json({ error: 'Approved agents cannot be updated. Create a new listing.', }); } const avatarFile = (req as any).file as MulterFile | undefined; const updateData = req.body; delete updateData.status; // Don't allow status updates via this endpoint delete updateData.creatorId; // Don't allow creator change delete updateData.reputation; // Reputation is calculated from ratings delete updateData.ratingCount; // Rating count is managed by system delete updateData.avatar; // Avatar comes from file upload, not body // Parse capabilities if it's a string if (updateData.capabilities) { if (typeof updateData.capabilities === 'string') { try { updateData.capabilities = JSON.parse(updateData.capabilities); } catch { updateData.capabilities = [updateData.capabilities]; } } } // Validate capabilities if provided if (updateData.capabilities && !Array.isArray(updateData.capabilities)) { return res.status(400).json({ error: 'Capabilities must be an array' }); } // Upload new avatar if provided if (avatarFile) { try { const fileObj = new File( [avatarFile.buffer], avatarFile.originalname || `avatar-${Date.now()}.${avatarFile.mimetype.split('/')[1]}`, { type: avatarFile.mimetype } ); const upload = await ipfsService.uploadFile(fileObj); updateData.avatar = ipfsService.getGatewayUrl(upload.cid); } catch (error) { console.error('Avatar upload error:', error); return res.status(500).json({ error: 'Failed to upload avatar image' }); } } Object.assign(agent, updateData); // Parse metadata if it's a string if (updateData.metadata && typeof updateData.metadata === 'string') { try { updateData.metadata = JSON.parse(updateData.metadata); } catch { // Keep as string if not valid JSON } } // Update IPFS if metadata changed if (updateData.name || updateData.description || updateData.price || updateData.category || updateData.capabilities) { try { agent.ipfsHash = await ipfsService.uploadMetadata({ name: agent.name, description: agent.description, endpoint: agent.endpoint, price: agent.price, creatorId: agent.creatorId, category: agent.category, capabilities: agent.capabilities, metadata: agent.metadata, }); } catch (error) { console.warn('IPFS update failed'); } } const updatedAgent = await agentRepository.save(agent); res.json(updatedAgent); } catch (error) { console.error('Update agent error:', error); res.status(500).json({ error: 'Failed to update agent' }); } } /** * Delete agent (creator only) */ async deleteAgent(req: Request, res: Response) { try { const { id } = req.params; const userId = (req as any).user.id; const isAdmin = (req as any).user.isAdmin; const agent = await agentRepository.findOne({ where: { id }, }); if (!agent) { return res.status(404).json({ error: 'Agent not found' }); } if (agent.creatorId !== userId && !isAdmin) { return res.status(403).json({ error: 'Unauthorized' }); } await agentRepository.remove(agent); res.json({ message: 'Agent deleted' }); } catch (error) { console.error('Delete agent error:', error); res.status(500).json({ error: 'Failed to delete agent' }); } } /** * Get user's agents (creator) */ async getMyAgents(req: Request, res: Response) { try { const userId = (req as any).user.id; const agents = await agentRepository.find({ where: { creatorId: userId }, order: { createdAt: 'DESC' }, }); res.json(agents); } catch (error) { console.error('Get my agents error:', error); res.status(500).json({ error: 'Failed to get your agents' }); } } }