diff --git a/examples/multi-agent-research/ARCHITECTURE.md b/examples/multi-agent-research/ARCHITECTURE.md new file mode 100644 index 0000000..08c661d --- /dev/null +++ b/examples/multi-agent-research/ARCHITECTURE.md @@ -0,0 +1,413 @@ +# Multi-Agent Research System Architecture + +## System Overview + +The multi-agent research system demonstrates a practical implementation of collaborative AI agents using the Agent App Framework. The architecture follows a coordinator-worker pattern where specialized agents work together to accomplish complex research tasks. + +## Component Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ User / Client │ +└────────────────────────┬────────────────────────────────────┘ + │ + │ HTTP POST /research + │ { query, depth } + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Coordinator Agent (Port 4000) │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Task Decomposition │ │ +│ │ - Analyzes query │ │ +│ │ - Creates subtasks │ │ +│ │ - Determines dependencies │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Agent Registry │ │ +│ │ - Tracks available agents │ │ +│ │ - Maps capabilities │ │ +│ │ - Monitors health │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Task Orchestration │ │ +│ │ - Delegates to specialists │ │ +│ │ - Manages dependencies │ │ +│ │ - Aggregates results │ │ +│ └─────────────────────────────────────────────────────┘ │ +└──────────────┬──────────────────────┬───────────────────────┘ + │ │ + │ │ + ┌──────────▼─────────┐ ┌────────▼──────────┐ + │ Researcher Agent │ │ Summarizer Agent │ + │ (Port 4001) │ │ (Port 4002) │ + │ │ │ │ + │ Capabilities: │ │ Capabilities: │ + │ - search │ │ - summarize │ + │ - validate │ │ - condense │ + │ - extract │ │ - format │ + └──────────┬─────────┘ └────────┬──────────┘ + │ │ + └──────────┬──────────┘ + │ + │ fishnet-auth + │ + ┌──────────▼──────────┐ + │ Agent App Server │ + │ (Port 3000) │ + │ │ + │ - Authentication │ + │ - API Endpoints │ + │ - Skill Discovery │ + └─────────────────────┘ +``` + +## Authentication Flow + +``` +┌──────────┐ ┌──────────┐ +│ Agent │ │ Server │ +└────┬─────┘ └────┬─────┘ + │ │ + │ GET /api/agent-auth?name=X │ + │──────────────────────────────>│ + │ │ + │ { tasks: [...] } │ + │<──────────────────────────────│ + │ │ + │ [Agent solves tasks] │ + │ │ + │ POST /api/agent-auth │ + │ { solutions: [...] } │ + │──────────────────────────────>│ + │ │ + │ { token, expiresIn } │ + │<──────────────────────────────│ + │ │ + │ [Agent uses token for API] │ + │ │ +``` + +## Task Execution Flow + +``` +1. Query Submission + User → Coordinator: POST /research + +2. Task Creation + Coordinator creates task with unique ID + +3. Task Decomposition + Coordinator breaks query into subtasks: + - Research subtask (priority 1) + - Summarize subtask (priority 2, depends on research) + +4. Agent Discovery + Coordinator finds agents by capability: + - search → ResearcherAgent + - summarize → SummarizerAgent + +5. Task Delegation + Coordinator → Researcher: POST /task + { + taskId: "uuid", + subtask: { + type: "research", + description: "Gather information about: ...", + capability: "search" + } + } + +6. Research Execution + Researcher conducts research + Researcher → Coordinator: { findings } + +7. Dependent Task Execution + Coordinator → Summarizer: POST /task + { + taskId: "uuid", + subtask: { + type: "summarize", + previousResults: { research: { findings } } + } + } + +8. Summary Generation + Summarizer processes findings + Summarizer → Coordinator: { summary } + +9. Result Aggregation + Coordinator combines all results + +10. Response Delivery + Coordinator → User: { report } +``` + +## State Management + +### Shared State Structure + +```javascript +{ + tasks: Map { + "task-id": { + id: "task-id", + status: "in-progress", + query: "...", + depth: "comprehensive", + createdAt: timestamp, + updatedAt: timestamp, + subtasks: [ + { + id: "task-id-0", + type: "research", + status: "completed", + result: {...} + }, + { + id: "task-id-1", + type: "summarize", + status: "in-progress" + } + ], + results: { + "ResearcherAgent": { + data: {...}, + timestamp: ... + } + } + } + }, + + agentStatus: Map { + "ResearcherAgent": { + name: "ResearcherAgent", + status: "online", + lastSeen: timestamp, + capabilities: ["search", "validate", "extract"] + } + } +} +``` + +## Communication Patterns + +### Agent-to-Server (fishnet-auth) +- **Protocol**: HTTP/HTTPS +- **Authentication**: fishnet-auth (reasoning tasks) +- **Token**: JWT Bearer token +- **Refresh**: Automatic before expiry + +### Coordinator-to-Agent (Task Delegation) +- **Protocol**: HTTP REST +- **Method**: POST /task +- **Timeout**: 30 seconds (configurable) +- **Retry**: 3 attempts with exponential backoff + +### Agent-to-Coordinator (Registration) +- **Protocol**: HTTP REST +- **Method**: POST /agent/register +- **Heartbeat**: Implicit via task execution + +## Scalability Considerations + +### Horizontal Scaling + +``` + ┌──────────────┐ + │ Load Balancer│ + └──────┬───────┘ + │ + ┌──────────────────┼──────────────────┐ + │ │ │ + ┌────▼─────┐ ┌────▼─────┐ ┌────▼─────┐ + │Researcher│ │Researcher│ │Researcher│ + │Instance 1│ │Instance 2│ │Instance 3│ + └──────────┘ └──────────┘ └──────────┘ +``` + +### Message Queue Integration + +For production deployments, replace direct HTTP calls with message queues: + +``` +Coordinator → [Task Queue] → Workers +Workers → [Result Queue] → Coordinator +``` + +Benefits: +- Decoupling +- Retry handling +- Load balancing +- Fault tolerance + +## Security Architecture + +### Authentication Layers + +1. **Agent Authentication**: fishnet-auth reasoning tasks +2. **Inter-Agent Communication**: Bearer tokens +3. **API Endpoints**: Rate limiting, input validation + +### Data Flow Security + +``` +User Request → HTTPS → Coordinator +Coordinator → Internal Network → Agents +Agents → HTTPS → Main Server (auth) +``` + +### Security Best Practices + +- Use HTTPS for all external communication +- Implement request signing for agent-to-agent calls +- Validate all inputs +- Rate limit API endpoints +- Rotate authentication tokens +- Log all agent interactions +- Implement circuit breakers + +## Monitoring & Observability + +### Key Metrics + +1. **Task Metrics** + - Task completion rate + - Average task duration + - Failed task percentage + - Queue depth + +2. **Agent Metrics** + - Agent availability + - Response time + - Error rate + - Active task count + +3. **System Metrics** + - Total throughput + - Resource utilization + - Authentication success rate + +### Logging Strategy + +``` +[Timestamp] [Component] [Level] Message +2026-02-10 10:00:00 [Coordinator] INFO Task created: uuid +2026-02-10 10:00:01 [Researcher] INFO Received task: uuid +2026-02-10 10:00:05 [Researcher] INFO Completed task: uuid +``` + +## Extension Points + +### Adding New Agent Types + +1. Create agent directory +2. Implement agent interface +3. Define capabilities +4. Register with coordinator +5. Update documentation + +### Custom Task Types + +1. Define task schema +2. Implement decomposition logic +3. Map to agent capabilities +4. Add result aggregation + +### External Integrations + +- Real API integrations (search, data sources) +- Database persistence (PostgreSQL, MongoDB) +- Caching layer (Redis) +- Message queues (RabbitMQ, Kafka) +- Monitoring (Prometheus, Grafana) + +## Deployment Architecture + +### Development + +``` +localhost:3000 → Main Server +localhost:4000 → Coordinator +localhost:4001 → Researcher +localhost:4002 → Summarizer +``` + +### Production + +``` +┌─────────────────────────────────────┐ +│ Load Balancer / CDN │ +└──────────────┬──────────────────────┘ + │ + ┌──────────┴──────────┐ + │ │ +┌───▼────┐ ┌────▼────┐ +│ Main │ │ Coord │ +│ Server │ │ Cluster │ +│ Cluster│ └────┬────┘ +└────────┘ │ + ┌────┴────┐ + │ │ + ┌─────▼──┐ ┌──▼──────┐ + │Research│ │Summarize│ + │Cluster │ │ Cluster │ + └────────┘ └─────────┘ +``` + +### Infrastructure Requirements + +- **Main Server**: 2 vCPU, 4GB RAM +- **Coordinator**: 2 vCPU, 4GB RAM +- **Each Agent**: 1 vCPU, 2GB RAM +- **Database**: PostgreSQL or MongoDB +- **Cache**: Redis +- **Message Queue**: RabbitMQ or Kafka (optional) + +## Performance Optimization + +### Caching Strategy + +- Cache agent discovery results +- Cache authentication tokens +- Cache frequently accessed research results + +### Parallel Execution + +- Execute independent subtasks in parallel +- Use worker pools for agent instances +- Implement connection pooling + +### Resource Management + +- Limit concurrent tasks per agent +- Implement backpressure mechanisms +- Use timeout and circuit breakers + +## Fault Tolerance + +### Failure Scenarios + +1. **Agent Failure**: Retry with different agent instance +2. **Network Failure**: Exponential backoff retry +3. **Timeout**: Mark subtask as failed, continue with others +4. **Authentication Failure**: Re-authenticate and retry + +### Recovery Strategies + +- Automatic task retry +- Fallback to alternative agents +- Graceful degradation +- State persistence for recovery + +## Future Enhancements + +1. **Dynamic Agent Discovery**: Service registry (Consul, etcd) +2. **Advanced Orchestration**: Workflow engine +3. **Machine Learning**: Agent selection optimization +4. **Real-time Updates**: WebSocket notifications +5. **Agent Marketplace**: Public agent registry +6. **Capability Negotiation**: Dynamic capability matching +7. **Cost Optimization**: Agent usage analytics diff --git a/examples/multi-agent-research/README.md b/examples/multi-agent-research/README.md new file mode 100644 index 0000000..d2da18b --- /dev/null +++ b/examples/multi-agent-research/README.md @@ -0,0 +1,359 @@ +# Multi-Agent Research Assistant + +A comprehensive example demonstrating multi-agent collaboration using the Agent App Framework. This example shows how multiple AI agents can work together to accomplish complex research tasks through authentication, discovery, and coordinated task execution. + +## Overview + +This example implements a research assistant system where multiple specialized agents collaborate: + +- **Coordinator Agent**: Receives research queries, breaks them into sub-tasks, and orchestrates specialist agents +- **Researcher Agent**: Gathers information from various sources +- **Summarizer Agent**: Condenses findings into concise summaries + +Each agent authenticates using fishnet-auth and communicates through a shared API, demonstrating real-world multi-agent patterns. + +## Architecture + +``` +┌─────────────────┐ +│ User/Client │ +└────────┬────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ Coordinator Agent │ +│ - Receives research query │ +│ - Breaks into sub-tasks │ +│ - Delegates to specialists │ +│ - Aggregates results │ +└──────┬──────────────────┬───────────┘ + │ │ + ▼ ▼ +┌──────────────┐ ┌──────────────┐ +│ Researcher │ │ Summarizer │ +│ Agent │ │ Agent │ +│ │ │ │ +│ - Searches │ │ - Condenses │ +│ - Validates │ │ - Formats │ +│ - Reports │ │ - Delivers │ +└──────────────┘ └──────────────┘ + │ │ + └──────┬───────────┘ + ▼ + ┌──────────────┐ + │ Shared State │ + │ Storage │ + └──────────────┘ +``` + +## Key Features + +### 1. Agent Discovery +Agents discover each other through skill.md endpoints, enabling dynamic collaboration without hardcoded dependencies. + +### 2. fishnet-auth Authentication +Each agent authenticates independently using reasoning tasks, proving they are legitimate AI agents. + +### 3. Task Delegation +The coordinator agent intelligently distributes work based on agent capabilities and current workload. + +### 4. State Synchronization +Shared state management ensures all agents have access to current research progress and findings. + +### 5. Error Handling +Robust error handling with retry logic and fallback strategies when agents are unavailable. + +## Quick Start + +### Prerequisites +- Node.js 18+ installed +- npm or pnpm +- The main agent-apps-experimental project set up + +### Setup + +1. **Install dependencies** (from project root): +```bash +npm install +``` + +2. **Configure environment variables**: +```bash +cp .env.example .env.local +# Set FISHNET_AUTH_SECRET to a strong random string +``` + +3. **Start the main server**: +```bash +npm run dev +``` + +4. **Run the coordinator agent**: +```bash +cd examples/multi-agent-research/coordinator +node index.js +``` + +5. **Run specialist agents** (in separate terminals): +```bash +# Terminal 2: Researcher +cd examples/multi-agent-research/researcher +node index.js + +# Terminal 3: Summarizer +cd examples/multi-agent-research/summarizer +node index.js +``` + +### Test the System + +Send a research query to the coordinator: + +```bash +curl -X POST http://localhost:4000/research \ + -H "Content-Type: application/json" \ + -d '{ + "query": "What are the latest developments in blockchain scalability?", + "depth": "comprehensive" + }' +``` + +## How It Works + +### 1. Query Submission +A user submits a research query to the Coordinator Agent. + +### 2. Task Decomposition +The Coordinator analyzes the query and breaks it into sub-tasks: +- Information gathering (→ Researcher) +- Summary generation (→ Summarizer) + +### 3. Agent Authentication +Each specialist agent authenticates with the main server using fishnet-auth: +```javascript +// Agent requests challenge +const challenge = await fetch('http://localhost:3000/api/agent-auth?name=ResearcherAgent'); + +// Agent solves reasoning tasks +const solutions = solveTasks(challenge.tasks); + +// Agent submits solutions and receives token +const { token } = await fetch('http://localhost:3000/api/agent-auth', { + method: 'POST', + body: JSON.stringify({ solutions }) +}); +``` + +### 4. Task Execution +Specialist agents execute their assigned tasks and report results back to the Coordinator. + +### 5. Result Aggregation +The Coordinator combines all results and delivers a comprehensive research report. + +## Agent Implementations + +### Coordinator Agent (`coordinator/index.js`) +- Receives research queries via REST API +- Decomposes queries into actionable sub-tasks +- Discovers available specialist agents +- Delegates tasks and tracks progress +- Aggregates results into final report + +### Researcher Agent (`researcher/index.js`) +- Authenticates with fishnet-auth +- Receives research tasks from Coordinator +- Simulates information gathering (can be extended with real APIs) +- Returns structured research findings + +### Summarizer Agent (`summarizer/index.js`) +- Authenticates with fishnet-auth +- Receives raw research data +- Generates concise summaries +- Formats output for readability + +## Shared Utilities (`shared/`) + +### `auth.js` +Handles fishnet-auth authentication flow: +- Challenge request +- Task solving +- Token management +- Token refresh + +### `discovery.js` +Agent discovery utilities: +- Fetches skill.md from agents +- Parses agent capabilities +- Maintains agent registry + +### `state.js` +Shared state management: +- In-memory state store (can be replaced with Redis/DB) +- State synchronization between agents +- Task status tracking + +## Configuration + +### Coordinator Agent +```javascript +// coordinator/config.js +module.exports = { + port: 4000, + serverUrl: 'http://localhost:3000', + maxConcurrentTasks: 5, + taskTimeout: 30000, // 30 seconds + retryAttempts: 3 +}; +``` + +### Specialist Agents +```javascript +// researcher/config.js +module.exports = { + agentName: 'ResearcherAgent', + capabilities: ['search', 'validate', 'extract'], + serverUrl: 'http://localhost:3000', + coordinatorUrl: 'http://localhost:4000' +}; +``` + +## Extending the Example + +### Add New Specialist Agents + +1. Create a new directory: `examples/multi-agent-research/your-agent/` +2. Implement the agent following the pattern in `researcher/index.js` +3. Define capabilities in the agent's skill.md +4. Register with the Coordinator + +### Integrate Real APIs + +Replace simulated research with real API calls: + +```javascript +// researcher/sources/web-search.js +async function searchWeb(query) { + // Integrate with search APIs (Google, Bing, etc.) + const results = await fetch(`https://api.search.com/query?q=${query}`); + return results; +} +``` + +### Add Persistence + +Replace in-memory state with a database: + +```javascript +// shared/state.js +const Redis = require('redis'); +const client = Redis.createClient(); + +async function saveState(taskId, state) { + await client.set(`task:${taskId}`, JSON.stringify(state)); +} +``` + +### Implement Agent Registry + +Create a service discovery system: + +```javascript +// shared/registry.js +class AgentRegistry { + async register(agentInfo) { + // Store agent metadata + // Health check endpoint + // Capability matching + } + + async findAgents(capability) { + // Return agents with matching capabilities + } +} +``` + +## Testing + +### Unit Tests +```bash +npm test +``` + +### Integration Tests +```bash +# Start all agents +npm run start:all + +# Run integration tests +npm run test:integration +``` + +### Manual Testing +Use the provided test script: +```bash +./test-multi-agent.sh +``` + +## Production Considerations + +### Security +- Use HTTPS for all agent communication +- Implement rate limiting +- Add request signing for agent-to-agent calls +- Validate all inputs + +### Scalability +- Deploy agents as separate services +- Use message queues for task distribution +- Implement load balancing +- Add horizontal scaling for specialist agents + +### Monitoring +- Track agent health and availability +- Monitor task completion rates +- Log all agent interactions +- Set up alerting for failures + +### Reliability +- Implement circuit breakers +- Add retry logic with exponential backoff +- Handle partial failures gracefully +- Maintain agent state across restarts + +## Troubleshooting + +### Agent Authentication Fails +- Verify FISHNET_AUTH_SECRET is set correctly +- Check that the agent is solving tasks correctly +- Ensure the server is running and accessible + +### Coordinator Can't Find Agents +- Verify all agents are running +- Check agent URLs in configuration +- Ensure skill.md endpoints are accessible + +### Tasks Timeout +- Increase taskTimeout in coordinator config +- Check agent performance and resource usage +- Verify network connectivity between agents + +## Learn More + +- [fishnet-auth Documentation](https://github.com/base/fishnet-auth) +- [Agent App Framework](../../README.md) +- [skill.md Specification](https://github.com/base/skill-md-spec) +- [Multi-Agent Systems Best Practices](https://example.com/multi-agent-best-practices) + +## Contributing + +Contributions are welcome! Areas for improvement: + +- Additional specialist agents (fact-checker, translator, etc.) +- Real API integrations +- Enhanced error handling +- Performance optimizations +- More comprehensive tests + +## License + +MIT - See LICENSE file in the project root diff --git a/examples/multi-agent-research/coordinator/config.js b/examples/multi-agent-research/coordinator/config.js new file mode 100644 index 0000000..db2f32d --- /dev/null +++ b/examples/multi-agent-research/coordinator/config.js @@ -0,0 +1,19 @@ +module.exports = { + // Coordinator server port + port: process.env.COORDINATOR_PORT || 4000, + + // Main agent app server URL + serverUrl: process.env.SERVER_URL || 'http://localhost:3000', + + // Maximum concurrent tasks + maxConcurrentTasks: parseInt(process.env.MAX_CONCURRENT_TASKS) || 5, + + // Task timeout in milliseconds + taskTimeout: parseInt(process.env.TASK_TIMEOUT) || 30000, + + // Number of retry attempts for failed tasks + retryAttempts: parseInt(process.env.RETRY_ATTEMPTS) || 3, + + // Retry delay in milliseconds + retryDelay: parseInt(process.env.RETRY_DELAY) || 1000, +}; diff --git a/examples/multi-agent-research/coordinator/index.js b/examples/multi-agent-research/coordinator/index.js new file mode 100644 index 0000000..e699d21 --- /dev/null +++ b/examples/multi-agent-research/coordinator/index.js @@ -0,0 +1,300 @@ +#!/usr/bin/env node + +/** + * Coordinator Agent + * Orchestrates research tasks by delegating to specialist agents + */ + +const express = require('express'); +const { v4: uuidv4 } = require('uuid'); +const stateManager = require('../shared/state'); +const config = require('./config'); + +const app = express(); +app.use(express.json()); + +// Track available agents +const agents = new Map(); + +/** + * Register an agent + */ +function registerAgent(agentInfo) { + agents.set(agentInfo.name, { + ...agentInfo, + lastSeen: Date.now(), + }); + stateManager.registerAgent(agentInfo.name, agentInfo); + console.log(`[Coordinator] Registered agent: ${agentInfo.name}`); +} + +/** + * Find agents by capability + */ +function findAgentsByCapability(capability) { + return Array.from(agents.values()).filter(agent => + agent.capabilities && agent.capabilities.includes(capability) + ); +} + +/** + * Decompose research query into subtasks + */ +function decomposeQuery(query, depth) { + const subtasks = []; + + // Always need research + subtasks.push({ + type: 'research', + description: `Gather information about: ${query}`, + capability: 'search', + priority: 1, + }); + + // Add summarization for comprehensive queries + if (depth === 'comprehensive' || depth === 'detailed') { + subtasks.push({ + type: 'summarize', + description: 'Summarize research findings', + capability: 'summarize', + priority: 2, + dependsOn: ['research'], + }); + } + + return subtasks; +} + +/** + * Delegate task to an agent + */ +async function delegateTask(taskId, subtask, agentUrl) { + try { + const response = await fetch(`${agentUrl}/task`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + taskId, + subtask, + }), + signal: AbortSignal.timeout(config.taskTimeout), + }); + + if (!response.ok) { + throw new Error(`Agent returned ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error(`[Coordinator] Failed to delegate task to ${agentUrl}:`, error.message); + throw error; + } +} + +/** + * Execute subtasks + */ +async function executeSubtasks(taskId, subtasks) { + const results = {}; + + // Execute tasks by priority + const sortedSubtasks = [...subtasks].sort((a, b) => a.priority - b.priority); + + for (const subtask of sortedSubtasks) { + // Check dependencies + if (subtask.dependsOn) { + const dependenciesMet = subtask.dependsOn.every(dep => results[dep]); + if (!dependenciesMet) { + console.log(`[Coordinator] Skipping ${subtask.type} - dependencies not met`); + continue; + } + } + + // Find capable agent + const capableAgents = findAgentsByCapability(subtask.capability); + if (capableAgents.length === 0) { + console.error(`[Coordinator] No agent found for capability: ${subtask.capability}`); + stateManager.updateSubtask(taskId, subtask.id, 'failed', { + error: 'No capable agent available', + }); + continue; + } + + // Use first available agent (can be improved with load balancing) + const agent = capableAgents[0]; + console.log(`[Coordinator] Delegating ${subtask.type} to ${agent.name}`); + + stateManager.updateSubtask(taskId, subtask.id, 'in-progress'); + + try { + // Include previous results for dependent tasks + const taskData = { + ...subtask, + previousResults: subtask.dependsOn ? + subtask.dependsOn.reduce((acc, dep) => { + acc[dep] = results[dep]; + return acc; + }, {}) : {}, + }; + + const result = await delegateTask(taskId, taskData, agent.url); + results[subtask.type] = result; + stateManager.updateSubtask(taskId, subtask.id, 'completed', result); + stateManager.storeResult(taskId, agent.name, result); + } catch (error) { + console.error(`[Coordinator] Task ${subtask.type} failed:`, error.message); + stateManager.updateSubtask(taskId, subtask.id, 'failed', { + error: error.message, + }); + } + } + + return results; +} + +/** + * POST /research - Submit research query + */ +app.post('/research', async (req, res) => { + try { + const { query, depth = 'basic' } = req.body; + + if (!query) { + return res.status(400).json({ error: 'Query is required' }); + } + + // Create task + const taskId = uuidv4(); + stateManager.createTask(taskId, { + query, + depth, + type: 'research', + }); + + console.log(`[Coordinator] New research task: ${taskId}`); + console.log(`[Coordinator] Query: ${query}`); + + // Decompose into subtasks + const subtasks = decomposeQuery(query, depth); + subtasks.forEach(subtask => { + stateManager.addSubtask(taskId, subtask); + }); + + // Update task status + stateManager.updateTask(taskId, 'in-progress'); + + // Execute subtasks + const results = await executeSubtasks(taskId, + stateManager.getTask(taskId).subtasks + ); + + // Aggregate results + const report = { + taskId, + query, + depth, + completedAt: new Date().toISOString(), + findings: results.research || {}, + summary: results.summarize || null, + }; + + stateManager.updateTask(taskId, 'completed', { report }); + + res.json({ + success: true, + taskId, + report, + }); + } catch (error) { + console.error('[Coordinator] Error processing research:', error); + res.status(500).json({ + error: 'Failed to process research query', + message: error.message, + }); + } +}); + +/** + * GET /task/:taskId - Get task status + */ +app.get('/task/:taskId', (req, res) => { + const task = stateManager.getTask(req.params.taskId); + + if (!task) { + return res.status(404).json({ error: 'Task not found' }); + } + + res.json(task); +}); + +/** + * POST /agent/register - Register an agent + */ +app.post('/agent/register', (req, res) => { + try { + const { name, url, capabilities } = req.body; + + if (!name || !url || !capabilities) { + return res.status(400).json({ + error: 'name, url, and capabilities are required', + }); + } + + registerAgent({ name, url, capabilities }); + + res.json({ + success: true, + message: `Agent ${name} registered successfully`, + }); + } catch (error) { + console.error('[Coordinator] Error registering agent:', error); + res.status(500).json({ + error: 'Failed to register agent', + message: error.message, + }); + } +}); + +/** + * GET /agents - List registered agents + */ +app.get('/agents', (req, res) => { + res.json({ + agents: Array.from(agents.values()), + online: stateManager.getOnlineAgents(), + }); +}); + +/** + * GET /stats - Get system statistics + */ +app.get('/stats', (req, res) => { + res.json(stateManager.getStats()); +}); + +/** + * GET /health - Health check + */ +app.get('/health', (req, res) => { + res.json({ + status: 'healthy', + uptime: process.uptime(), + agents: agents.size, + }); +}); + +// Start server +const PORT = config.port || 4000; +app.listen(PORT, () => { + console.log(`[Coordinator] Listening on port ${PORT}`); + console.log(`[Coordinator] Server URL: ${config.serverUrl}`); + console.log(`[Coordinator] Waiting for agents to register...`); +}); + +// Graceful shutdown +process.on('SIGINT', () => { + console.log('\n[Coordinator] Shutting down gracefully...'); + process.exit(0); +}); diff --git a/examples/multi-agent-research/coordinator/package.json b/examples/multi-agent-research/coordinator/package.json new file mode 100644 index 0000000..90844a8 --- /dev/null +++ b/examples/multi-agent-research/coordinator/package.json @@ -0,0 +1,25 @@ +{ + "name": "coordinator-agent", + "version": "1.0.0", + "description": "Coordinator agent for multi-agent research system", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js" + }, + "keywords": [ + "agent", + "coordinator", + "multi-agent", + "research" + ], + "author": "", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "uuid": "^9.0.0" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/examples/multi-agent-research/researcher/config.js b/examples/multi-agent-research/researcher/config.js new file mode 100644 index 0000000..bf7e7a8 --- /dev/null +++ b/examples/multi-agent-research/researcher/config.js @@ -0,0 +1,16 @@ +module.exports = { + // Agent identity + agentName: process.env.AGENT_NAME || 'ResearcherAgent', + + // Agent capabilities + capabilities: ['search', 'validate', 'extract'], + + // Server port + port: process.env.PORT || 4001, + + // Main agent app server URL + serverUrl: process.env.SERVER_URL || 'http://localhost:3000', + + // Coordinator URL + coordinatorUrl: process.env.COORDINATOR_URL || 'http://localhost:4000', +}; diff --git a/examples/multi-agent-research/researcher/index.js b/examples/multi-agent-research/researcher/index.js new file mode 100644 index 0000000..4c3f386 --- /dev/null +++ b/examples/multi-agent-research/researcher/index.js @@ -0,0 +1,230 @@ +#!/usr/bin/env node + +/** + * Researcher Agent + * Gathers information for research queries + */ + +const express = require('express'); +const AgentAuth = require('../shared/auth'); +const config = require('./config'); + +const app = express(); +app.use(express.json()); + +// Initialize authentication +const auth = new AgentAuth(config.serverUrl, config.agentName); + +// Agent state +let isAuthenticated = false; +let currentTasks = new Map(); + +/** + * Simulate research (replace with real API calls in production) + */ +async function conductResearch(query) { + console.log(`[Researcher] Conducting research on: ${query}`); + + // Simulate research delay + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Simulated research results + const findings = { + query, + sources: [ + { + title: `Understanding ${query}`, + url: 'https://example.com/article1', + summary: `This article provides an overview of ${query}, covering key concepts and recent developments.`, + relevance: 0.95, + }, + { + title: `Latest Developments in ${query}`, + url: 'https://example.com/article2', + summary: `Recent advancements and breakthrough research in the field of ${query}.`, + relevance: 0.88, + }, + { + title: `${query}: A Comprehensive Guide`, + url: 'https://example.com/article3', + summary: `An in-depth guide covering all aspects of ${query} with practical examples.`, + relevance: 0.82, + }, + ], + keyPoints: [ + `${query} is an evolving field with significant recent progress`, + 'Multiple approaches and methodologies are being explored', + 'Practical applications are expanding rapidly', + ], + confidence: 0.85, + timestamp: new Date().toISOString(), + }; + + return findings; +} + +/** + * POST /task - Receive and execute research task + */ +app.post('/task', async (req, res) => { + try { + const { taskId, subtask } = req.body; + + if (!taskId || !subtask) { + return res.status(400).json({ + error: 'taskId and subtask are required', + }); + } + + console.log(`[Researcher] Received task ${taskId}: ${subtask.description}`); + + // Store task + currentTasks.set(taskId, { + ...subtask, + status: 'in-progress', + startedAt: Date.now(), + }); + + // Extract query from description or use previous results + const query = subtask.query || + subtask.description.replace('Gather information about: ', ''); + + // Conduct research + const findings = await conductResearch(query); + + // Update task status + currentTasks.set(taskId, { + ...currentTasks.get(taskId), + status: 'completed', + completedAt: Date.now(), + result: findings, + }); + + console.log(`[Researcher] Completed task ${taskId}`); + + res.json({ + success: true, + taskId, + findings, + }); + } catch (error) { + console.error('[Researcher] Error processing task:', error); + + if (currentTasks.has(req.body.taskId)) { + currentTasks.set(req.body.taskId, { + ...currentTasks.get(req.body.taskId), + status: 'failed', + error: error.message, + }); + } + + res.status(500).json({ + error: 'Failed to process research task', + message: error.message, + }); + } +}); + +/** + * GET /task/:taskId - Get task status + */ +app.get('/task/:taskId', (req, res) => { + const task = currentTasks.get(req.params.taskId); + + if (!task) { + return res.status(404).json({ error: 'Task not found' }); + } + + res.json(task); +}); + +/** + * GET /capabilities - Return agent capabilities + */ +app.get('/capabilities', (req, res) => { + res.json({ + name: config.agentName, + capabilities: config.capabilities, + status: isAuthenticated ? 'authenticated' : 'unauthenticated', + activeTasks: currentTasks.size, + }); +}); + +/** + * GET /health - Health check + */ +app.get('/health', (req, res) => { + res.json({ + status: 'healthy', + authenticated: isAuthenticated, + uptime: process.uptime(), + activeTasks: currentTasks.size, + }); +}); + +/** + * Initialize agent + */ +async function initialize() { + try { + console.log(`[Researcher] Starting ${config.agentName}...`); + + // Authenticate with main server + console.log('[Researcher] Authenticating with server...'); + await auth.authenticate(); + isAuthenticated = true; + + // Start HTTP server + const PORT = config.port || 4001; + app.listen(PORT, async () => { + console.log(`[Researcher] Listening on port ${PORT}`); + + // Register with coordinator + try { + const response = await fetch(`${config.coordinatorUrl}/agent/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: config.agentName, + url: `http://localhost:${PORT}`, + capabilities: config.capabilities, + }), + }); + + if (response.ok) { + console.log('[Researcher] ✓ Registered with coordinator'); + } else { + console.warn('[Researcher] Failed to register with coordinator'); + } + } catch (error) { + console.warn('[Researcher] Could not reach coordinator:', error.message); + } + }); + + // Refresh token periodically + setInterval(async () => { + try { + await auth.getToken(); + } catch (error) { + console.error('[Researcher] Token refresh failed:', error.message); + isAuthenticated = false; + } + }, 600000); // Every 10 minutes + + } catch (error) { + console.error('[Researcher] Initialization failed:', error); + process.exit(1); + } +} + +// Start agent +initialize(); + +// Graceful shutdown +process.on('SIGINT', () => { + console.log('\n[Researcher] Shutting down gracefully...'); + auth.logout(); + process.exit(0); +}); diff --git a/examples/multi-agent-research/researcher/package.json b/examples/multi-agent-research/researcher/package.json new file mode 100644 index 0000000..0463cdc --- /dev/null +++ b/examples/multi-agent-research/researcher/package.json @@ -0,0 +1,23 @@ +{ + "name": "researcher-agent", + "version": "1.0.0", + "description": "Researcher agent for multi-agent research system", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js" + }, + "keywords": [ + "agent", + "researcher", + "multi-agent" + ], + "author": "", + "license": "MIT", + "dependencies": { + "express": "^4.18.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/examples/multi-agent-research/shared/auth.js b/examples/multi-agent-research/shared/auth.js new file mode 100644 index 0000000..97ca9b2 --- /dev/null +++ b/examples/multi-agent-research/shared/auth.js @@ -0,0 +1,226 @@ +/** + * Shared authentication utilities for multi-agent system + * Handles fishnet-auth authentication flow + */ + +class AgentAuth { + constructor(serverUrl, agentName) { + this.serverUrl = serverUrl; + this.agentName = agentName; + this.token = null; + this.tokenExpiry = null; + } + + /** + * Authenticate with the server using fishnet-auth + * @returns {Promise} Bearer token + */ + async authenticate() { + try { + // Step 1: Request challenge + console.log(`[${this.agentName}] Requesting authentication challenge...`); + const challengeResponse = await fetch( + `${this.serverUrl}/api/agent-auth?name=${encodeURIComponent(this.agentName)}` + ); + + if (!challengeResponse.ok) { + throw new Error(`Failed to get challenge: ${challengeResponse.statusText}`); + } + + const challenge = await challengeResponse.json(); + console.log(`[${this.agentName}] Received ${challenge.tasks?.length || 0} tasks`); + + // Step 2: Solve tasks + const solutions = this.solveTasks(challenge.tasks); + + // Step 3: Submit solutions + console.log(`[${this.agentName}] Submitting solutions...`); + const authResponse = await fetch(`${this.serverUrl}/api/agent-auth`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: this.agentName, + solutions, + }), + }); + + if (!authResponse.ok) { + const error = await authResponse.text(); + throw new Error(`Authentication failed: ${error}`); + } + + const { token, expiresIn } = await authResponse.json(); + this.token = token; + this.tokenExpiry = Date.now() + (expiresIn * 1000); + + console.log(`[${this.agentName}] ✓ Authenticated successfully`); + return token; + } catch (error) { + console.error(`[${this.agentName}] Authentication error:`, error.message); + throw error; + } + } + + /** + * Solve fishnet-auth reasoning tasks + * @param {Array} tasks - Array of reasoning tasks + * @returns {Array} Solutions to the tasks + */ + solveTasks(tasks) { + return tasks.map((task) => { + switch (task.type) { + case 'reverse': + // Reverse a string + return task.input.split('').reverse().join(''); + + case 'sort': + // Sort an array + return [...task.input].sort((a, b) => { + if (typeof a === 'number') return a - b; + return String(a).localeCompare(String(b)); + }); + + case 'math': + // Evaluate mathematical expression + // Note: In production, use a safe math parser + try { + // Simple evaluation for basic operations + return this.evaluateMath(task.input); + } catch (e) { + console.error(`[${this.agentName}] Math task failed:`, e); + return null; + } + + case 'pattern': + // Find pattern in sequence + return this.findPattern(task.input); + + case 'logic': + // Solve logic puzzle + return this.solveLogic(task.input); + + default: + console.warn(`[${this.agentName}] Unknown task type: ${task.type}`); + return null; + } + }); + } + + /** + * Evaluate simple mathematical expressions + * @param {string} expression - Math expression + * @returns {number} Result + */ + evaluateMath(expression) { + // Simple safe math evaluation + const cleaned = expression.replace(/[^0-9+\-*/().\s]/g, ''); + // In production, use a proper math parser library + return Function(`'use strict'; return (${cleaned})`)(); + } + + /** + * Find pattern in number sequence + * @param {Array} sequence - Number sequence + * @returns {number} Next number in sequence + */ + findPattern(sequence) { + if (sequence.length < 2) return sequence[0]; + + // Check for arithmetic sequence + const diff = sequence[1] - sequence[0]; + let isArithmetic = true; + for (let i = 2; i < sequence.length; i++) { + if (sequence[i] - sequence[i - 1] !== diff) { + isArithmetic = false; + break; + } + } + if (isArithmetic) { + return sequence[sequence.length - 1] + diff; + } + + // Check for geometric sequence + if (sequence[0] !== 0) { + const ratio = sequence[1] / sequence[0]; + let isGeometric = true; + for (let i = 2; i < sequence.length; i++) { + if (sequence[i - 1] === 0 || sequence[i] / sequence[i - 1] !== ratio) { + isGeometric = false; + break; + } + } + if (isGeometric) { + return sequence[sequence.length - 1] * ratio; + } + } + + // Default: return last element + return sequence[sequence.length - 1]; + } + + /** + * Solve simple logic puzzles + * @param {Object} puzzle - Logic puzzle + * @returns {*} Solution + */ + solveLogic(puzzle) { + // Implement logic puzzle solving + // This is a placeholder for more complex logic + return puzzle.answer || true; + } + + /** + * Get current authentication token + * Automatically re-authenticates if token is expired + * @returns {Promise} Valid bearer token + */ + async getToken() { + if (!this.token || this.isTokenExpired()) { + await this.authenticate(); + } + return this.token; + } + + /** + * Check if token is expired or about to expire + * @returns {boolean} True if token needs refresh + */ + isTokenExpired() { + if (!this.tokenExpiry) return true; + // Refresh if less than 5 minutes remaining + return Date.now() > this.tokenExpiry - 300000; + } + + /** + * Make authenticated request to server + * @param {string} endpoint - API endpoint + * @param {Object} options - Fetch options + * @returns {Promise} Fetch response + */ + async authenticatedFetch(endpoint, options = {}) { + const token = await this.getToken(); + + const headers = { + ...options.headers, + 'Authorization': `Bearer ${token}`, + }; + + return fetch(`${this.serverUrl}${endpoint}`, { + ...options, + headers, + }); + } + + /** + * Revoke current token + */ + logout() { + this.token = null; + this.tokenExpiry = null; + console.log(`[${this.agentName}] Logged out`); + } +} + +module.exports = AgentAuth; diff --git a/examples/multi-agent-research/shared/state.js b/examples/multi-agent-research/shared/state.js new file mode 100644 index 0000000..30d2287 --- /dev/null +++ b/examples/multi-agent-research/shared/state.js @@ -0,0 +1,315 @@ +/** + * Shared state management for multi-agent system + * In-memory implementation (can be replaced with Redis/DB for production) + */ + +class StateManager { + constructor() { + this.tasks = new Map(); + this.sessions = new Map(); + this.agentStatus = new Map(); + } + + /** + * Create a new research task + * @param {string} taskId - Unique task identifier + * @param {Object} taskData - Task data + */ + createTask(taskId, taskData) { + this.tasks.set(taskId, { + id: taskId, + status: 'pending', + createdAt: Date.now(), + updatedAt: Date.now(), + ...taskData, + subtasks: [], + results: {}, + }); + } + + /** + * Get task by ID + * @param {string} taskId - Task identifier + * @returns {Object|null} Task data + */ + getTask(taskId) { + return this.tasks.get(taskId) || null; + } + + /** + * Update task status + * @param {string} taskId - Task identifier + * @param {string} status - New status + * @param {Object} updates - Additional updates + */ + updateTask(taskId, status, updates = {}) { + const task = this.tasks.get(taskId); + if (!task) { + throw new Error(`Task ${taskId} not found`); + } + + this.tasks.set(taskId, { + ...task, + status, + updatedAt: Date.now(), + ...updates, + }); + } + + /** + * Add subtask to a task + * @param {string} taskId - Parent task ID + * @param {Object} subtask - Subtask data + */ + addSubtask(taskId, subtask) { + const task = this.tasks.get(taskId); + if (!task) { + throw new Error(`Task ${taskId} not found`); + } + + task.subtasks.push({ + id: `${taskId}-${task.subtasks.length}`, + status: 'pending', + createdAt: Date.now(), + ...subtask, + }); + + task.updatedAt = Date.now(); + } + + /** + * Update subtask status + * @param {string} taskId - Parent task ID + * @param {string} subtaskId - Subtask ID + * @param {string} status - New status + * @param {Object} result - Subtask result + */ + updateSubtask(taskId, subtaskId, status, result = null) { + const task = this.tasks.get(taskId); + if (!task) { + throw new Error(`Task ${taskId} not found`); + } + + const subtask = task.subtasks.find(st => st.id === subtaskId); + if (!subtask) { + throw new Error(`Subtask ${subtaskId} not found`); + } + + subtask.status = status; + subtask.updatedAt = Date.now(); + if (result) { + subtask.result = result; + } + + task.updatedAt = Date.now(); + } + + /** + * Store result for a task + * @param {string} taskId - Task identifier + * @param {string} agentName - Agent that produced the result + * @param {*} result - Result data + */ + storeResult(taskId, agentName, result) { + const task = this.tasks.get(taskId); + if (!task) { + throw new Error(`Task ${taskId} not found`); + } + + task.results[agentName] = { + data: result, + timestamp: Date.now(), + }; + + task.updatedAt = Date.now(); + } + + /** + * Get all results for a task + * @param {string} taskId - Task identifier + * @returns {Object} Results by agent name + */ + getResults(taskId) { + const task = this.tasks.get(taskId); + return task ? task.results : {}; + } + + /** + * Check if all subtasks are complete + * @param {string} taskId - Task identifier + * @returns {boolean} True if all subtasks are complete + */ + areSubtasksComplete(taskId) { + const task = this.tasks.get(taskId); + if (!task || task.subtasks.length === 0) { + return false; + } + + return task.subtasks.every(st => + st.status === 'completed' || st.status === 'failed' + ); + } + + /** + * Create a new session + * @param {string} sessionId - Session identifier + * @param {Object} sessionData - Session data + */ + createSession(sessionId, sessionData) { + this.sessions.set(sessionId, { + id: sessionId, + createdAt: Date.now(), + updatedAt: Date.now(), + ...sessionData, + }); + } + + /** + * Get session by ID + * @param {string} sessionId - Session identifier + * @returns {Object|null} Session data + */ + getSession(sessionId) { + return this.sessions.get(sessionId) || null; + } + + /** + * Update session + * @param {string} sessionId - Session identifier + * @param {Object} updates - Session updates + */ + updateSession(sessionId, updates) { + const session = this.sessions.get(sessionId); + if (!session) { + throw new Error(`Session ${sessionId} not found`); + } + + this.sessions.set(sessionId, { + ...session, + ...updates, + updatedAt: Date.now(), + }); + } + + /** + * Register agent status + * @param {string} agentName - Agent name + * @param {Object} status - Agent status + */ + registerAgent(agentName, status) { + this.agentStatus.set(agentName, { + name: agentName, + status: 'online', + lastSeen: Date.now(), + ...status, + }); + } + + /** + * Update agent status + * @param {string} agentName - Agent name + * @param {Object} updates - Status updates + */ + updateAgentStatus(agentName, updates) { + const agent = this.agentStatus.get(agentName); + if (!agent) { + this.registerAgent(agentName, updates); + return; + } + + this.agentStatus.set(agentName, { + ...agent, + ...updates, + lastSeen: Date.now(), + }); + } + + /** + * Get agent status + * @param {string} agentName - Agent name + * @returns {Object|null} Agent status + */ + getAgentStatus(agentName) { + return this.agentStatus.get(agentName) || null; + } + + /** + * Get all online agents + * @returns {Array} List of online agents + */ + getOnlineAgents() { + const now = Date.now(); + const timeout = 60000; // 1 minute + + return Array.from(this.agentStatus.values()).filter( + agent => agent.status === 'online' && (now - agent.lastSeen) < timeout + ); + } + + /** + * Clean up old tasks and sessions + * @param {number} maxAge - Maximum age in milliseconds + */ + cleanup(maxAge = 3600000) { // Default: 1 hour + const now = Date.now(); + + // Clean up old tasks + for (const [taskId, task] of this.tasks.entries()) { + if (now - task.updatedAt > maxAge && + (task.status === 'completed' || task.status === 'failed')) { + this.tasks.delete(taskId); + } + } + + // Clean up old sessions + for (const [sessionId, session] of this.sessions.entries()) { + if (now - session.updatedAt > maxAge) { + this.sessions.delete(sessionId); + } + } + + // Mark stale agents as offline + for (const [agentName, agent] of this.agentStatus.entries()) { + if (now - agent.lastSeen > 60000) { // 1 minute + agent.status = 'offline'; + } + } + } + + /** + * Get statistics + * @returns {Object} System statistics + */ + getStats() { + const tasks = Array.from(this.tasks.values()); + const agents = Array.from(this.agentStatus.values()); + + return { + tasks: { + total: tasks.length, + pending: tasks.filter(t => t.status === 'pending').length, + inProgress: tasks.filter(t => t.status === 'in-progress').length, + completed: tasks.filter(t => t.status === 'completed').length, + failed: tasks.filter(t => t.status === 'failed').length, + }, + sessions: { + total: this.sessions.size, + }, + agents: { + total: agents.length, + online: agents.filter(a => a.status === 'online').length, + offline: agents.filter(a => a.status === 'offline').length, + }, + }; + } +} + +// Singleton instance +const stateManager = new StateManager(); + +// Auto cleanup every 5 minutes +setInterval(() => { + stateManager.cleanup(); +}, 300000); + +module.exports = stateManager; diff --git a/examples/multi-agent-research/summarizer/config.js b/examples/multi-agent-research/summarizer/config.js new file mode 100644 index 0000000..27ff9c1 --- /dev/null +++ b/examples/multi-agent-research/summarizer/config.js @@ -0,0 +1,16 @@ +module.exports = { + // Agent identity + agentName: process.env.AGENT_NAME || 'SummarizerAgent', + + // Agent capabilities + capabilities: ['summarize', 'condense', 'format'], + + // Server port + port: process.env.PORT || 4002, + + // Main agent app server URL + serverUrl: process.env.SERVER_URL || 'http://localhost:3000', + + // Coordinator URL + coordinatorUrl: process.env.COORDINATOR_URL || 'http://localhost:4000', +}; diff --git a/examples/multi-agent-research/summarizer/index.js b/examples/multi-agent-research/summarizer/index.js new file mode 100644 index 0000000..648373f --- /dev/null +++ b/examples/multi-agent-research/summarizer/index.js @@ -0,0 +1,246 @@ +#!/usr/bin/env node + +/** + * Summarizer Agent + * Condenses research findings into concise summaries + */ + +const express = require('express'); +const AgentAuth = require('../shared/auth'); +const config = require('./config'); + +const app = express(); +app.use(express.json()); + +// Initialize authentication +const auth = new AgentAuth(config.serverUrl, config.agentName); + +// Agent state +let isAuthenticated = false; +let currentTasks = new Map(); + +/** + * Generate summary from research findings + */ +function generateSummary(findings) { + console.log('[Summarizer] Generating summary...'); + + if (!findings || !findings.sources) { + return { + error: 'No findings to summarize', + }; + } + + // Extract key information + const query = findings.query || 'the topic'; + const sourceCount = findings.sources.length; + const keyPoints = findings.keyPoints || []; + + // Generate executive summary + const executiveSummary = `Research on ${query} reveals ${keyPoints.length} key insights from ${sourceCount} sources. ` + + `The findings indicate ${keyPoints[0] || 'significant developments in this area'}.`; + + // Generate detailed summary + const detailedSummary = { + overview: executiveSummary, + keyFindings: keyPoints, + sourceAnalysis: findings.sources.map(source => ({ + title: source.title, + mainPoint: source.summary.split('.')[0] + '.', + relevance: source.relevance, + })), + confidence: findings.confidence || 0.8, + recommendations: [ + 'Further research is recommended to explore emerging trends', + 'Cross-reference findings with additional authoritative sources', + 'Monitor ongoing developments in this field', + ], + }; + + // Generate concise summary + const conciseSummary = keyPoints.join('. ') + '.'; + + return { + query, + executive: executiveSummary, + detailed: detailedSummary, + concise: conciseSummary, + wordCount: { + executive: executiveSummary.split(' ').length, + concise: conciseSummary.split(' ').length, + }, + generatedAt: new Date().toISOString(), + }; +} + +/** + * POST /task - Receive and execute summarization task + */ +app.post('/task', async (req, res) => { + try { + const { taskId, subtask } = req.body; + + if (!taskId || !subtask) { + return res.status(400).json({ + error: 'taskId and subtask are required', + }); + } + + console.log(`[Summarizer] Received task ${taskId}: ${subtask.description}`); + + // Store task + currentTasks.set(taskId, { + ...subtask, + status: 'in-progress', + startedAt: Date.now(), + }); + + // Get research findings from previous results + const findings = subtask.previousResults?.research?.findings; + + if (!findings) { + throw new Error('No research findings provided'); + } + + // Simulate processing time + await new Promise(resolve => setTimeout(resolve, 1500)); + + // Generate summary + const summary = generateSummary(findings); + + // Update task status + currentTasks.set(taskId, { + ...currentTasks.get(taskId), + status: 'completed', + completedAt: Date.now(), + result: summary, + }); + + console.log(`[Summarizer] Completed task ${taskId}`); + + res.json({ + success: true, + taskId, + summary, + }); + } catch (error) { + console.error('[Summarizer] Error processing task:', error); + + if (currentTasks.has(req.body.taskId)) { + currentTasks.set(req.body.taskId, { + ...currentTasks.get(req.body.taskId), + status: 'failed', + error: error.message, + }); + } + + res.status(500).json({ + error: 'Failed to process summarization task', + message: error.message, + }); + } +}); + +/** + * GET /task/:taskId - Get task status + */ +app.get('/task/:taskId', (req, res) => { + const task = currentTasks.get(req.params.taskId); + + if (!task) { + return res.status(404).json({ error: 'Task not found' }); + } + + res.json(task); +}); + +/** + * GET /capabilities - Return agent capabilities + */ +app.get('/capabilities', (req, res) => { + res.json({ + name: config.agentName, + capabilities: config.capabilities, + status: isAuthenticated ? 'authenticated' : 'unauthenticated', + activeTasks: currentTasks.size, + }); +}); + +/** + * GET /health - Health check + */ +app.get('/health', (req, res) => { + res.json({ + status: 'healthy', + authenticated: isAuthenticated, + uptime: process.uptime(), + activeTasks: currentTasks.size, + }); +}); + +/** + * Initialize agent + */ +async function initialize() { + try { + console.log(`[Summarizer] Starting ${config.agentName}...`); + + // Authenticate with main server + console.log('[Summarizer] Authenticating with server...'); + await auth.authenticate(); + isAuthenticated = true; + + // Start HTTP server + const PORT = config.port || 4002; + app.listen(PORT, async () => { + console.log(`[Summarizer] Listening on port ${PORT}`); + + // Register with coordinator + try { + const response = await fetch(`${config.coordinatorUrl}/agent/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: config.agentName, + url: `http://localhost:${PORT}`, + capabilities: config.capabilities, + }), + }); + + if (response.ok) { + console.log('[Summarizer] ✓ Registered with coordinator'); + } else { + console.warn('[Summarizer] Failed to register with coordinator'); + } + } catch (error) { + console.warn('[Summarizer] Could not reach coordinator:', error.message); + } + }); + + // Refresh token periodically + setInterval(async () => { + try { + await auth.getToken(); + } catch (error) { + console.error('[Summarizer] Token refresh failed:', error.message); + isAuthenticated = false; + } + }, 600000); // Every 10 minutes + + } catch (error) { + console.error('[Summarizer] Initialization failed:', error); + process.exit(1); + } +} + +// Start agent +initialize(); + +// Graceful shutdown +process.on('SIGINT', () => { + console.log('\n[Summarizer] Shutting down gracefully...'); + auth.logout(); + process.exit(0); +}); diff --git a/examples/multi-agent-research/summarizer/package.json b/examples/multi-agent-research/summarizer/package.json new file mode 100644 index 0000000..7d2944d --- /dev/null +++ b/examples/multi-agent-research/summarizer/package.json @@ -0,0 +1,23 @@ +{ + "name": "summarizer-agent", + "version": "1.0.0", + "description": "Summarizer agent for multi-agent research system", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js" + }, + "keywords": [ + "agent", + "summarizer", + "multi-agent" + ], + "author": "", + "license": "MIT", + "dependencies": { + "express": "^4.18.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/examples/multi-agent-research/test-multi-agent.sh b/examples/multi-agent-research/test-multi-agent.sh new file mode 100755 index 0000000..65ab63c --- /dev/null +++ b/examples/multi-agent-research/test-multi-agent.sh @@ -0,0 +1,156 @@ +#!/bin/bash + +# Test script for multi-agent research system + +set -e + +echo "=========================================" +echo "Multi-Agent Research System Test" +echo "=========================================" +echo "" + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Configuration +COORDINATOR_URL="http://localhost:4000" +RESEARCHER_URL="http://localhost:4001" +SUMMARIZER_URL="http://localhost:4002" + +# Function to check if service is running +check_service() { + local url=$1 + local name=$2 + + echo -n "Checking $name... " + if curl -s "$url/health" > /dev/null 2>&1; then + echo -e "${GREEN}✓ Running${NC}" + return 0 + else + echo -e "${RED}✗ Not running${NC}" + return 1 + fi +} + +# Function to wait for service +wait_for_service() { + local url=$1 + local name=$2 + local max_attempts=30 + local attempt=0 + + echo -n "Waiting for $name to start... " + while [ $attempt -lt $max_attempts ]; do + if curl -s "$url/health" > /dev/null 2>&1; then + echo -e "${GREEN}✓ Ready${NC}" + return 0 + fi + sleep 1 + attempt=$((attempt + 1)) + done + + echo -e "${RED}✗ Timeout${NC}" + return 1 +} + +echo "Step 1: Checking services" +echo "-------------------------" + +# Check if services are running +services_ok=true +check_service "$COORDINATOR_URL" "Coordinator" || services_ok=false +check_service "$RESEARCHER_URL" "Researcher" || services_ok=false +check_service "$SUMMARIZER_URL" "Summarizer" || services_ok=false + +if [ "$services_ok" = false ]; then + echo "" + echo -e "${YELLOW}Warning: Some services are not running${NC}" + echo "Please start all agents before running this test:" + echo " Terminal 1: cd coordinator && npm start" + echo " Terminal 2: cd researcher && npm start" + echo " Terminal 3: cd summarizer && npm start" + echo "" + exit 1 +fi + +echo "" +echo "Step 2: Checking agent registration" +echo "------------------------------------" + +agents_response=$(curl -s "$COORDINATOR_URL/agents") +echo "Registered agents:" +echo "$agents_response" | jq '.' + +echo "" +echo "Step 3: Submitting research query" +echo "----------------------------------" + +query="What are the latest developments in blockchain scalability?" +echo "Query: $query" +echo "" + +response=$(curl -s -X POST "$COORDINATOR_URL/research" \ + -H "Content-Type: application/json" \ + -d "{\"query\": \"$query\", \"depth\": \"comprehensive\"}") + +echo "Response:" +echo "$response" | jq '.' + +# Extract task ID +task_id=$(echo "$response" | jq -r '.taskId') + +if [ "$task_id" = "null" ] || [ -z "$task_id" ]; then + echo -e "${RED}✗ Failed to create task${NC}" + exit 1 +fi + +echo "" +echo -e "${GREEN}✓ Task created: $task_id${NC}" + +echo "" +echo "Step 4: Checking task status" +echo "-----------------------------" + +task_status=$(curl -s "$COORDINATOR_URL/task/$task_id") +echo "$task_status" | jq '.' + +echo "" +echo "Step 5: Verifying results" +echo "-------------------------" + +# Check if task completed successfully +status=$(echo "$task_status" | jq -r '.status') +if [ "$status" = "completed" ]; then + echo -e "${GREEN}✓ Task completed successfully${NC}" + + # Display summary + echo "" + echo "Research Summary:" + echo "----------------" + echo "$task_status" | jq -r '.report.summary.executive' + + echo "" + echo "Key Findings:" + echo "$task_status" | jq -r '.report.summary.detailed.keyFindings[]' | while read -r line; do + echo " • $line" + done + +else + echo -e "${RED}✗ Task status: $status${NC}" + exit 1 +fi + +echo "" +echo "Step 6: System statistics" +echo "-------------------------" + +stats=$(curl -s "$COORDINATOR_URL/stats") +echo "$stats" | jq '.' + +echo "" +echo "=========================================" +echo -e "${GREEN}All tests passed!${NC}" +echo "========================================="