zurri / src /controllers /agentController.ts
nexusbert's picture
push
f5035b9
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' });
}
}
}