|
|
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 { |
|
|
|
|
|
|
|
|
|
|
|
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}%` } |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
const formattedAgents = agents.map(agent => ({ |
|
|
...agent, |
|
|
avatar: agent.avatar, |
|
|
reputation: agent.reputation || 0, |
|
|
capabilities: agent.capabilities || [], |
|
|
|
|
|
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' }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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' }); |
|
|
} |
|
|
|
|
|
|
|
|
const userId = (req as any).user?.id; |
|
|
if (agent.creatorId !== userId && !(req as any).user?.isAdmin) { |
|
|
|
|
|
const { endpoint, ...agentWithoutEndpoint } = agent; |
|
|
res.json({ |
|
|
...agentWithoutEndpoint, |
|
|
|
|
|
avatar: agentWithoutEndpoint.avatar, |
|
|
reputation: agent.reputation || 0, |
|
|
capabilities: agent.capabilities || [], |
|
|
}); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
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' }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async createAgent(req: Request, res: Response) { |
|
|
try { |
|
|
const userId = (req as any).user.id; |
|
|
const isAdmin = (req as any).user.isAdmin; |
|
|
|
|
|
|
|
|
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, |
|
|
isSubscription = false, |
|
|
subscriptionDuration, |
|
|
promptTemplate, |
|
|
metadata, |
|
|
} = req.body; |
|
|
|
|
|
|
|
|
let parsedCapabilities: string[] = []; |
|
|
if (capabilities) { |
|
|
if (typeof capabilities === 'string') { |
|
|
try { |
|
|
parsedCapabilities = JSON.parse(capabilities); |
|
|
} catch { |
|
|
parsedCapabilities = [capabilities]; |
|
|
} |
|
|
} else if (Array.isArray(capabilities)) { |
|
|
parsedCapabilities = capabilities; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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)' |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (pointsPerTask < 0) { |
|
|
return res.status(400).json({ error: 'pointsPerTask must be 0 or greater (0 = free agent)' }); |
|
|
} |
|
|
|
|
|
|
|
|
if (!Array.isArray(parsedCapabilities)) { |
|
|
return res.status(400).json({ error: 'Capabilities must be an array' }); |
|
|
} |
|
|
|
|
|
|
|
|
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' }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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'); |
|
|
} |
|
|
|
|
|
|
|
|
const finalPointsPerTask = pointsPerTask !== undefined ? pointsPerTask : (price ? price * 20 : 0); |
|
|
|
|
|
const agent = agentRepository.create({ |
|
|
name, |
|
|
avatar: avatarUrl, |
|
|
description, |
|
|
endpoint, |
|
|
category, |
|
|
capabilities: parsedCapabilities, |
|
|
price: price || pointsPerTask * 0.05, |
|
|
pointsPerTask: finalPointsPerTask, |
|
|
isSubscription: false, |
|
|
subscriptionDuration: undefined, |
|
|
promptTemplate, |
|
|
metadata: metadata ? (typeof metadata === 'string' ? JSON.parse(metadata) : metadata) : undefined, |
|
|
creatorId: userId, |
|
|
ipfsHash, |
|
|
status: AgentStatus.PENDING, |
|
|
reputation: 0, |
|
|
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' }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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' }); |
|
|
} |
|
|
|
|
|
|
|
|
if (agent.creatorId !== userId && !isAdmin) { |
|
|
return res.status(403).json({ error: 'Unauthorized' }); |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
delete updateData.creatorId; |
|
|
delete updateData.reputation; |
|
|
delete updateData.ratingCount; |
|
|
delete updateData.avatar; |
|
|
|
|
|
|
|
|
if (updateData.capabilities) { |
|
|
if (typeof updateData.capabilities === 'string') { |
|
|
try { |
|
|
updateData.capabilities = JSON.parse(updateData.capabilities); |
|
|
} catch { |
|
|
updateData.capabilities = [updateData.capabilities]; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (updateData.capabilities && !Array.isArray(updateData.capabilities)) { |
|
|
return res.status(400).json({ error: 'Capabilities must be an array' }); |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
if (updateData.metadata && typeof updateData.metadata === 'string') { |
|
|
try { |
|
|
updateData.metadata = JSON.parse(updateData.metadata); |
|
|
} catch { |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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' }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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' }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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' }); |
|
|
} |
|
|
} |
|
|
} |
|
|
|