331 lines
8.7 KiB
TypeScript
331 lines
8.7 KiB
TypeScript
// ============================================================
|
|
// A2A SERVER - ROUTES
|
|
// Endpoints REST para o Agent to Agent Protocol
|
|
// ============================================================
|
|
|
|
import { Router, Request, Response } from 'express';
|
|
import * as a2aService from './service';
|
|
import * as a2aClient from './client';
|
|
|
|
const router = Router();
|
|
|
|
/**
|
|
* GET /.well-known/agent.json
|
|
* Retorna o Agent Card do Arcadia Suite
|
|
* (Padrão A2A para descoberta de agentes)
|
|
*/
|
|
router.get('/agent.json', (req: Request, res: Response) => {
|
|
try {
|
|
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
|
const agentCard = a2aService.getAgentCard(baseUrl);
|
|
res.json(agentCard);
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /a2a/v1/tasks
|
|
* Cria uma nova tarefa (SendMessage)
|
|
*/
|
|
router.post('/tasks', async (req: Request, res: Response) => {
|
|
try {
|
|
const { message, taskId, metadata } = req.body;
|
|
|
|
if (!message || !message.parts || !Array.isArray(message.parts)) {
|
|
return res.status(400).json({
|
|
error: 'Mensagem inválida. Formato esperado: { message: { parts: [{ type: "text", text: "..." }] } }'
|
|
});
|
|
}
|
|
|
|
// Se taskId fornecido, continua conversa existente
|
|
if (taskId) {
|
|
const existingTask = a2aService.getTask(taskId);
|
|
if (!existingTask) {
|
|
return res.status(404).json({ error: `Tarefa '${taskId}' não encontrada` });
|
|
}
|
|
|
|
// Adiciona mensagem ao histórico
|
|
a2aService.addMessageToTask(taskId, { ...message, role: 'user' });
|
|
|
|
// Processa a mensagem de forma assíncrona
|
|
a2aService.processTask(taskId).catch(console.error);
|
|
|
|
return res.status(202).json({ task: a2aService.getTask(taskId) });
|
|
}
|
|
|
|
// Cria nova tarefa
|
|
const task = await a2aService.createTask({ ...message, role: 'user' }, metadata);
|
|
|
|
// Processa a tarefa de forma assíncrona
|
|
a2aService.processTask(task.id).catch(console.error);
|
|
|
|
// Retorna imediatamente com status 202 (Accepted)
|
|
res.status(202).json({ task });
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /a2a/v1/tasks/:id
|
|
* Obtém o status de uma tarefa
|
|
*/
|
|
router.get('/tasks/:id', (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const task = a2aService.getTask(id);
|
|
|
|
if (!task) {
|
|
return res.status(404).json({ error: `Tarefa '${id}' não encontrada` });
|
|
}
|
|
|
|
res.json({ task });
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /a2a/v1/tasks
|
|
* Lista todas as tarefas com paginação
|
|
*/
|
|
router.get('/tasks', (req: Request, res: Response) => {
|
|
try {
|
|
const page = parseInt(req.query.page as string) || 1;
|
|
const pageSize = parseInt(req.query.pageSize as string) || 20;
|
|
|
|
const result = a2aService.listTasks(page, pageSize);
|
|
res.json(result);
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /a2a/v1/tasks/:id/cancel
|
|
* Cancela uma tarefa
|
|
*/
|
|
router.post('/tasks/:id/cancel', (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const task = a2aService.cancelTask(id);
|
|
|
|
if (!task) {
|
|
return res.status(404).json({ error: `Tarefa '${id}' não encontrada` });
|
|
}
|
|
|
|
res.json({ task });
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /a2a/v1/tasks/:id/stream
|
|
* Stream de eventos da tarefa via Server-Sent Events (SSE)
|
|
*/
|
|
router.get('/tasks/:id/stream', (req: Request, res: Response) => {
|
|
const { id } = req.params;
|
|
const task = a2aService.getTask(id);
|
|
|
|
if (!task) {
|
|
return res.status(404).json({ error: `Tarefa '${id}' não encontrada` });
|
|
}
|
|
|
|
// Configura headers para SSE
|
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
res.setHeader('Cache-Control', 'no-cache');
|
|
res.setHeader('Connection', 'keep-alive');
|
|
res.setHeader('X-Accel-Buffering', 'no');
|
|
|
|
// Envia evento inicial com estado atual
|
|
res.write(`data: ${JSON.stringify({ type: 'initial', task })}\n\n`);
|
|
|
|
// Registra listener para eventos
|
|
const unsubscribe = a2aService.subscribeToTask(id, (event) => {
|
|
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
|
|
// Se a tarefa terminou, fecha a conexão
|
|
if (event.type === 'state_change' &&
|
|
['completed', 'failed', 'canceled'].includes(event.data.state)) {
|
|
res.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
|
|
res.end();
|
|
}
|
|
});
|
|
|
|
// Cleanup quando a conexão é fechada
|
|
req.on('close', () => {
|
|
unsubscribe();
|
|
});
|
|
});
|
|
|
|
/**
|
|
* GET /a2a/v1/stats
|
|
* Retorna estatísticas do servidor A2A
|
|
*/
|
|
router.get('/stats', (req: Request, res: Response) => {
|
|
try {
|
|
const stats = a2aService.getServerStats();
|
|
res.json(stats);
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /a2a/v1/health
|
|
* Health check do servidor A2A
|
|
*/
|
|
router.get('/health', (req: Request, res: Response) => {
|
|
res.json({
|
|
status: 'healthy',
|
|
timestamp: new Date().toISOString(),
|
|
protocol: 'A2A',
|
|
version: '1.0.0'
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// ROTAS DO CLIENTE A2A (para gerenciar agentes externos)
|
|
// ============================================================
|
|
|
|
/**
|
|
* GET /a2a/v1/agents
|
|
* Lista todos os agentes externos registrados
|
|
*/
|
|
router.get('/agents', (req: Request, res: Response) => {
|
|
try {
|
|
const agents = a2aClient.listAgents();
|
|
const stats = a2aClient.getClientStats();
|
|
res.json({ agents, stats });
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /a2a/v1/agents
|
|
* Registra um novo agente externo
|
|
*/
|
|
router.post('/agents', async (req: Request, res: Response) => {
|
|
try {
|
|
const { id, name, url, apiKey, description } = req.body;
|
|
|
|
if (!name || !url) {
|
|
return res.status(400).json({ error: 'Nome e URL são obrigatórios' });
|
|
}
|
|
|
|
const agent = await a2aClient.registerAgent({
|
|
id: id || name.toLowerCase().replace(/\s+/g, '-'),
|
|
name,
|
|
url,
|
|
apiKey,
|
|
description,
|
|
status: 'inactive'
|
|
});
|
|
|
|
res.status(201).json({ agent });
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /a2a/v1/agents/:id
|
|
* Obtém detalhes de um agente específico
|
|
*/
|
|
router.get('/agents/:id', (req: Request, res: Response) => {
|
|
try {
|
|
const agent = a2aClient.getAgent(req.params.id);
|
|
if (!agent) {
|
|
return res.status(404).json({ error: 'Agente não encontrado' });
|
|
}
|
|
res.json({ agent });
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /a2a/v1/agents/:id
|
|
* Remove um agente externo
|
|
*/
|
|
router.delete('/agents/:id', (req: Request, res: Response) => {
|
|
try {
|
|
const removed = a2aClient.unregisterAgent(req.params.id);
|
|
if (!removed) {
|
|
return res.status(404).json({ error: 'Agente não encontrado' });
|
|
}
|
|
res.json({ success: true, message: 'Agente removido' });
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /a2a/v1/agents/:id/message
|
|
* Envia mensagem para um agente externo
|
|
*/
|
|
router.post('/agents/:id/message', async (req: Request, res: Response) => {
|
|
try {
|
|
const { message, waitForCompletion = true, timeout = 120000 } = req.body;
|
|
|
|
if (!message) {
|
|
return res.status(400).json({ error: 'Mensagem é obrigatória' });
|
|
}
|
|
|
|
const result = await a2aClient.sendMessageToAgent(req.params.id, message, {
|
|
waitForCompletion,
|
|
timeout
|
|
});
|
|
|
|
res.json(result);
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /a2a/v1/agents/discover
|
|
* Descobre o Agent Card de uma URL
|
|
*/
|
|
router.post('/agents/discover', async (req: Request, res: Response) => {
|
|
try {
|
|
const { url } = req.body;
|
|
|
|
if (!url) {
|
|
return res.status(400).json({ error: 'URL é obrigatória' });
|
|
}
|
|
|
|
const agentCard = await a2aClient.discoverAgent(url);
|
|
|
|
if (!agentCard) {
|
|
return res.status(404).json({ error: 'Agent Card não encontrado' });
|
|
}
|
|
|
|
res.json({ agentCard });
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /a2a/v1/agents/health-check
|
|
* Verifica a saúde de todos os agentes
|
|
*/
|
|
router.post('/agents/health-check', async (req: Request, res: Response) => {
|
|
try {
|
|
const results = await a2aClient.healthCheckAllAgents();
|
|
const healthStatus: Record<string, boolean> = {};
|
|
results.forEach((healthy, id) => {
|
|
healthStatus[id] = healthy;
|
|
});
|
|
res.json({ results: healthStatus });
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
export default router;
|