Complex Multi-Agent Workflow: House Buying System
May 5, 2025 ยท View on GitHub
*** Please note this tutorial is still work in progres, I will update it shortly ***
Overview
This tutorial demonstrates how to build a sophisticated multi-agent system using A2AJava and Tools4AI. We'll create a house buying workflow that showcases advanced features like:
- Dynamic task processing
- Real-time agent communication
- Risk management
- Human-in-loop approvals
- Server-Sent Events (SSE) for real-time updates
1. Configuration Setup
First, configure your Tools4AI properties:
# tools4ai.properties
agent.provider=gemini
agent.risk.level.default=LOW
agent.human.approval.required=true
2. Task Processing Infrastructure
Base Task Controller
@Service
public class HouseBuyingTaskController extends DyanamicTaskContoller {
@Autowired
private RiskManager riskManager;
@Override
public SendTaskResponse sendTask(TaskSendParams taskSendParams, ActionCallback callback) {
String taskId = taskSendParams.getId();
Task task = new Task();
task.setId(taskId);
task.setSessionId(UUID.randomUUID().toString());
// Configure real-time updates
task.setPushNotificationConfig(new TaskPushNotificationConfig());
// Process task asynchronously
nonBlockingService.execute(() -> {
try {
TextPart textPart = (TextPart) taskSendParams.getMessage().getParts().get(0);
String prompt = textPart.getText();
SSEEmitterCallback sseCallback = new SSEEmitterCallback(taskId, getEmitter(taskId));
sseCallback.setContext(task);
// Process with risk awareness
if (riskManager.requiresApproval(task, getCurrentAgent())) {
processHighRiskTask(task, prompt, sseCallback);
} else {
getBaseProcessor().processSingleAction(prompt, sseCallback);
}
} catch (Exception e) {
handleTaskError(task, e);
}
});
return createResponse(task);
}
}
3. Real-Time Updates Implementation
SSE Callback Handler
public class SSEEmitterCallback implements ActionCallback {
private final String taskId;
private final SseEmitter emitter;
private Task context;
@Override
public void sendtStatus(String status, ActionState state) {
try {
TaskStatusUpdateEvent event = new TaskStatusUpdateEvent(
taskId,
new TaskStatus(state.name(), status),
false
);
emitter.send(SseEmitter.event()
.name("status_update")
.data(event));
} catch (IOException e) {
log.severe("Failed to send SSE update: " + e.getMessage());
}
}
@Override
public void setContext(Object context) {
if (context instanceof Task) {
this.context = (Task) context;
}
}
}
4. Risk Management Integration
Risk-Aware Action Processing
@Component
public class HouseBuyingRiskManager extends RiskManager {
@Override
public boolean requiresApproval(Task task, Agent agent) {
if (task.getType() == TaskType.MAKE_OFFER) {
Map<String, Object> params = task.getParameters();
double offerAmount = Double.parseDouble(params.get("amount").toString());
// High-risk conditions
if (offerAmount > 1000000) {
return true;
}
// Medium-risk conditions requiring additional checks
if (offerAmount > 500000) {
return validateMediumRiskOffer(params);
}
}
return false;
}
private boolean validateMediumRiskOffer(Map<String, Object> params) {
// Implement medium-risk validation logic
// For example, check buyer's credit score, down payment, etc.
return false;
}
}
5. Human-in-Loop Integration
Approval Workflow
@Service
public class HouseOfferApprovalService {
@Autowired
private StateMachine<ApprovalStates, ApprovalEvents> stateMachine;
@Autowired
private HumanInLoop humanInLoop;
public void processOffer(Task offerTask) {
FeedbackLoop feedback = humanInLoop.allow(
createApprovalPrompt(offerTask),
"validateOffer",
offerTask.getParameters()
);
feedback.setCallback(new ActionCallback() {
@Override
public void onComplete(ActionState state) {
if (state == ActionState.APPROVED) {
stateMachine.sendEvent(ApprovalEvents.APPROVE);
proceedWithOffer(offerTask);
} else {
stateMachine.sendEvent(ApprovalEvents.REJECT);
handleRejectedOffer(offerTask);
}
}
});
}
private String createApprovalPrompt(Task task) {
return String.format(
"High-value offer requiring approval:\n" +
"Property: %s\n" +
"Offer Amount: $%s\n" +
"Buyer: %s\n" +
"Please review and approve/reject.",
task.getParameters().get("propertyId"),
task.getParameters().get("amount"),
task.getParameters().get("buyerId")
);
}
}
6. Event Handling and Monitoring
Event Listener Implementation
@Component
public class HouseBuyingEventListener {
@EventListener
public void onStateChange(StateMachineEvent event) {
logStateTransition(event);
notifyRelevantParties(event);
updateTaskStatus(event);
}
@EventListener
public void onRiskDetected(RiskDetectedEvent event) {
// Handle risk events
escalateIfNeeded(event);
updateRiskMetrics(event);
}
@Async
public void notifyRelevantParties(StateMachineEvent event) {
// Implement notification logic
// For example, send emails, push notifications, etc.
}
}
7. Complete Usage Example
@RestController
@RequestMapping("/api/house-buying")
public class HouseBuyingController {
@Autowired
private HouseBuyingWorkflowService workflowService;
@Autowired
private HouseOfferApprovalService approvalService;
@PostMapping("/make-offer")
public ResponseEntity<String> makeOffer(@RequestBody OfferRequest request) {
// Create and validate the task
Task offerTask = createOfferTask(request);
// Start the workflow
if (workflowService.requiresHumanApproval(offerTask)) {
approvalService.processOffer(offerTask);
return ResponseEntity.ok("Offer submitted for approval");
} else {
workflowService.processOffer(offerTask);
return ResponseEntity.ok("Offer processed automatically");
}
}
@GetMapping("/updates/{taskId}")
public SseEmitter subscribeToUpdates(@PathVariable String taskId) {
return workflowService.createUpdateEmitter(taskId);
}
}
8. MCP (Model Context Protocol) Integration
MCP Tools Controller
@Service
public class HouseBuyingMCPController extends MCPToolsController {
@Override
public CallToolResult callTool(ToolCallRequest request, ActionCallback callback) {
// Configure AI processing based on tools4ai.properties
AIProcessor processor = getBaseProcessor();
Map<String, AIAction> predictions = PredictionLoader.getInstance().getPredictions();
AIAction action = predictions.get(request.getName());
try {
// Process with human verification and explanation
Object result = processor.processSingleAction(
request.toString(),
action,
new LoggingHumanDecision(),
new LogginggExplainDecision(),
callback
);
return createToolResponse(result, callback);
} catch (AIProcessingException e) {
return handleAIError(e);
}
}
private CallToolResult createToolResponse(Object result, ActionCallback callback) {
CallToolResult callToolResult = new CallToolResult();
List<Content> content = new ArrayList<>();
TextContent textContent = new TextContent();
textContent.setText(result.toString());
textContent.setType("text");
content.add(textContent);
callToolResult.setContent(content);
callback.setContext(callToolResult);
return callToolResult;
}
}
AI Action Annotations
@Agent(groupName = "house-valuation", groupDescription = "Property valuation and analysis")
public class PropertyValuationAgent {
@Action(name = "estimatePropertyValue",
description = "Estimates property value based on features and market data",
risk = ActionRisk.MEDIUM)
public ValuationResult estimatePropertyValue(
@ActionParameter(name = "propertyFeatures", description = "Property details including location, size, etc.")
PropertyFeatures features,
@ActionParameter(name = "marketData", description = "Current market conditions and comparable sales")
MarketData marketData) {
// Implementation
}
}
MCP Callback Implementation
public class MCPHouseBuyingCallback implements MCPActionCallback {
private final Task task;
@Override
public void setContext(Object obj) {
if (obj instanceof CallToolResult) {
updateTaskWithToolResult((CallToolResult) obj);
}
}
private void updateTaskWithToolResult(CallToolResult result) {
TaskStatus status = new TaskStatus(TaskState.COMPLETED);
Message message = new Message();
message.setParts(convertContentToParts(result.getContent()));
status.setMessage(message);
task.setStatus(status);
}
}
9. AI Model Integration Examples
Property Value Estimation
@Service
public class PropertyAIService {
@Autowired
private AIProcessor aiProcessor;
public ValuationEstimate getPropertyValuation(PropertyDetails details) {
String prompt = String.format(
"Analyze the following property details and provide a valuation estimate:\n" +
"Location: %s\n" +
"Square Footage: %d\n" +
"Bedrooms: %d\n" +
"Year Built: %d\n" +
"Recent Renovations: %s",
details.getLocation(),
details.getSquareFootage(),
details.getBedrooms(),
details.getYearBuilt(),
details.getRenovations()
);
try {
AIAction action = PredictionLoader.getInstance()
.getPredictedAction(prompt, AIPlatform.GEMINI);
return (ValuationEstimate) aiProcessor.processSingleAction(
prompt,
action,
new PropertyValuationHumanVerification(),
new ValuationExplanationProvider()
);
} catch (AIProcessingException e) {
log.severe("AI Valuation failed: " + e.getMessage());
throw new ValuationException("Failed to estimate property value", e);
}
}
}
Market Analysis Integration
@Service
public class MarketAnalysisService {
@Autowired
private AIProcessor aiProcessor;
public MarketReport analyzeMarketConditions(String location) {
String prompt = createMarketAnalysisPrompt(location);
try {
return (MarketReport) aiProcessor.processSingleAction(
prompt,
new MarketAnalysisCallback()
);
} catch (AIProcessingException e) {
handleAnalysisError(e);
return null;
}
}
private String createMarketAnalysisPrompt(String location) {
return String.format(
"Analyze current market conditions for %s including:\n" +
"1. Average home prices\n" +
"2. Price trends over last 6 months\n" +
"3. Average days on market\n" +
"4. Inventory levels\n" +
"5. Buyer/seller market indicators",
location
);
}
}
This MCP integration enables:
- AI-powered property valuation
- Market analysis and predictions
- Risk assessment automation
- Decision support for agents
- Automated document analysis
- Smart contract recommendations
The combination of A2AJava's task management and Tools4AI's AI capabilities creates a powerful platform for intelligent real estate operations.
Best Practices and Advanced Features
-
Error Handling and Recovery
- Implement compensating transactions
- Use reliable messaging patterns
- Maintain audit logs
-
Scalability Considerations
- Use asynchronous processing
- Implement proper task queuing
- Consider distributed state management
-
Security Best Practices
- Implement proper authentication
- Use role-based access control
- Validate all inputs
-
Monitoring and Maintenance
- Track task processing metrics
- Monitor state transitions
- Set up alerting for failures
Integration Tips
- Configure proper timeouts for long-running tasks:
@Configuration
public class TaskConfig {
@Bean
public SseEmitter createEmitter() {
return new SseEmitter(Long.MAX_VALUE);
}
@Bean
public ExecutorService taskExecutor() {
return Executors.newCachedThreadPool();
}
}
- Implement proper cleanup:
@PreDestroy
public void cleanup() {
emitters.forEach((id, emitter) -> {
try {
emitter.complete();
} catch (Exception e) {
log.warning("Error during emitter cleanup: " + e.getMessage());
}
});
}
Testing Considerations
- Unit Testing:
@Test
public void testHighRiskOfferRequiresApproval() {
Task task = createHighValueOfferTask();
assertTrue(riskManager.requiresApproval(task, buyerAgent));
}
- Integration Testing:
@SpringBootTest
public class WorkflowIntegrationTest {
@Test
public void testCompleteWorkflow() {
// Test the entire workflow
OfferRequest request = createTestOffer();
SendTaskResponse response = workflowService.processOffer(request);
assertNotNull(response.getTaskId());
// Verify state transitions
}
}
Conclusion
This implementation demonstrates how to:
- Build complex multi-agent systems
- Handle real-time updates
- Manage risks
- Integrate human approval workflows
- Scale and monitor the system
Next steps could include:
- Adding more specialized agents
- Implementing advanced analytics
- Adding blockchain integration
- Implementing machine learning for risk assessment