A Spring Boot application built on the Qodo Agent Core framework for processing webhooks and orchestrating AI agents.
This application demonstrates how to build production-ready AI agent integrations using the Agent Core framework. The included examples (Jira and CloudWatch integrations) show patterns for processing events through custom handlers that orchestrate AI agents for tasks like code generation, bug analysis, and log monitoring.
Built on Spring Boot, this application leverages the full Spring ecosystem - REST controllers, JMS messaging, Spring Data, Spring Security, and more. Add your own integrations using familiar Spring patterns: REST endpoints, message listeners, scheduled tasks, database repositories, or custom Spring beans. The examples provided demonstrate the patterns, but you can integrate with any system or build entirely custom workflows.
- Spring Boot Foundation - Leverage the entire Spring ecosystem (Web, Data, Security, Cloud, etc.)
- Flexible Integration - REST APIs, JMS/ActiveMQ, scheduled jobs, database triggers, Kafka consumers, gRPC services
- Deterministic Workflows - Map agent responses to predictable business logic and outcomes
- Event-Driven Architecture - Async message processing with guaranteed delivery and retry logic
- Extensible Handler System - Create custom handlers for domain-specific agent orchestration
- Production-Ready - Built-in monitoring, health checks, and observability with Prometheus/Grafana
- Cloud-Native - Containerized deployment with Docker Compose or Kubernetes
- Java 21+
- Docker and Docker Compose (for containerized deployment)
- Access to local Maven repository (for Agent Core dependency)
./gradlew build./gradlew bootRuncd docker
make fast # Fast build for development
make run # Start all servicesThe application uses the Agent Core framework. Version is configured in gradle.properties:
# gradle.properties
internalCoreVersion=2.0.5
mcpInternalVersion=1.0.3To update to a newer version:
- Update the version in
gradle.properties - Rebuild:
./gradlew build --refresh-dependencies - Test and deploy
qodo-app/
├── src/main/java/ai/qodo/app/
│ ├── Application.java # Main Spring Boot application
│ ├── config/ # Configuration classes
│ │ ├── JiraAgentProperties.java # Jira webhook settings
│ │ ├── WebhookProperties.java # General webhook config
│ │ └── WebSecurityConfig.java # Security configuration
│ ├── handlers/ # Agent message handlers
│ │ ├── JiraAgentHandler.java # Processes Jira events
│ │ ├── CodingAgentHandler.java # Handles code generation
│ │ ├── CloudWatchAgentHandler.java # Processes AWS logs
│ │ └── IncompleteAgentResponseHandler.java
│ └── controllers/ # REST API endpoints
│ ├── JiraWebhookController.java # Jira webhook endpoint
│ ├── JiraWebhookValidator.java # Webhook signature validation
│ └── CloudWatchLogWebhookController.java
├── src/main/resources/
│ └── application.yml # Application configuration
├── docker/ # Docker build system
│ ├── Dockerfile.app # Optimized build
│ ├── Dockerfile.app-fast # Fast development build
│ ├── docker-compose.yml # Multi-service orchestration
│ ├── Makefile # Build commands
│ └── agent.yml # Agent configuration
└── mcp/ # MCP server tools
The application uses a layered configuration system:
- Agent Core defaults - Base framework configuration
- Application overrides - Custom settings in
src/main/resources/application.yml
Key configuration properties:
# Jira webhook configuration
jira:
webhook:
secret: ${JIRA_WEBHOOK_SECRET}
validate-signature: ${JIRA_WEBHOOK_VALIDATION_ENABLED:false}
agent:
account-id: ${JIRA_AGENT_ACCOUNT_ID:}
block-enabled: ${JIRA_BLOCK_AGENT_WEBHOOKS:true}
# Messaging configuration
messaging:
provider: ${MESSAGING_PROVIDER:activemq}
activemq:
broker-url: ${MESSAGING_ACTIVEMQ_BROKER_URL:tcp://localhost:61616}
# MCP (Model Context Protocol) Configuration
qodo:
mcp:
request-timeout-seconds: ${QODO_MCP_REQUEST_TIMEOUT_SECONDS:300}Required:
JIRA_WEBHOOK_SECRET- Secret for validating Jira webhook signatures
Optional:
JIRA_WEBHOOK_VALIDATION_ENABLED- Enable/disable signature validation (default: false)JIRA_AGENT_ACCOUNT_ID- Jira account ID to block from triggering webhooksJIRA_BLOCK_AGENT_WEBHOOKS- Prevent webhook loops (default: true)MESSAGING_PROVIDER- Message broker type:activemqorlocal(default: activemq)MESSAGING_ACTIVEMQ_BROKER_URL- ActiveMQ connection URLQODO_MCP_REQUEST_TIMEOUT_SECONDS- MCP request timeout (default: 300)GITHUB_API_TOKEN- GitHub API token for agent operationsSNYK_TOKEN- Snyk API token for security scanning
See docker/.env.example for a complete list of environment variables.
This application serves as a reference implementation demonstrating how to build production-ready integrations on top of the Agent Core framework. The included examples (Jira, CloudWatch) are just starting points - the real power comes from adapting these patterns to your specific environment.
DevOps & Incident Management:
- PagerDuty alerts → Automated root cause analysis → Slack notification with remediation steps
- Datadog anomaly detection → Agent investigates logs → Creates Jira ticket with findings
- GitHub PR opened → Security scan + code review → Auto-comment with suggestions
Project Management:
- Jira bug created → Agent analyzes stack trace → Suggests fix + creates PR
- Linear issue updated → Agent checks dependencies → Updates related tickets
- Asana task assigned → Agent gathers context → Prepares briefing document
Monitoring & Observability:
- CloudWatch alarm triggered → Agent analyzes metrics → Determines if scaling needed
- Prometheus alert → Agent correlates with recent deployments → Identifies culprit
- Sentry error → Agent reproduces issue → Generates test case
The key to production reliability is mapping non-deterministic agent outputs to deterministic business logic. Here's how:
@Service("incidentAnalysisHandler" + Handler.HANDLER_SUFFIX)
@Scope("prototype")
public class IncidentAnalysisHandler extends BaseHandler {
@Override
public Map<String, Object> handle(Map<String, Object> message) {
// Agent provides analysis (non-deterministic)
String agentResponse = (String) message.get("analysis");
// Map to deterministic outcomes
IncidentSeverity severity = classifySeverity(agentResponse);
List<String> actionItems = extractActionItems(agentResponse);
// Trigger predictable workflows based on classification
switch (severity) {
case CRITICAL -> escalateToOnCall(actionItems);
case HIGH -> createUrgentTicket(actionItems);
case MEDIUM -> scheduleForNextSprint(actionItems);
case LOW -> addToBacklog(actionItems);
}
return buildResponse(severity, actionItems);
}
private IncidentSeverity classifySeverity(String analysis) {
// Use keywords, confidence scores, or secondary validation
if (analysis.contains("production down") || analysis.contains("data loss")) {
return IncidentSeverity.CRITICAL;
}
// ... more classification logic
}
}@Service("codeReviewHandler" + Handler.HANDLER_SUFFIX)
@Scope("prototype")
public class CodeReviewHandler extends BaseHandler {
@Override
public Map<String, Object> handle(Map<String, Object> message) {
String prUrl = (String) message.get("pull_request_url");
// Stage 1: Agent performs initial review
AgentResponse initialReview = invokeAgent("code_review", prUrl);
// Stage 2: Validate findings with static analysis
List<Issue> validatedIssues = validateWithSonarQube(initialReview.getIssues());
// Stage 3: Check against team standards
List<Issue> policyViolations = checkAgainstPolicy(validatedIssues);
// Stage 4: Deterministic decision
if (policyViolations.isEmpty() && validatedIssues.size() < 5) {
approvePR(prUrl);
} else {
requestChanges(prUrl, policyViolations);
}
return buildResponse(validatedIssues, policyViolations);
}
}@Service("bugTriageHandler" + Handler.HANDLER_SUFFIX)
@Scope("prototype")
public class BugTriageHandler extends BaseHandler {
@Override
public Map<String, Object> handle(Map<String, Object> message) {
String bugDescription = (String) message.get("description");
// Agent analyzes bug
TriageResult result = invokeAgent("bug_triage", bugDescription);
// Route based on confidence level
if (result.getConfidence() > 0.9) {
// High confidence: Auto-assign to team
assignToTeam(result.getTeam(), result.getReasoning());
} else if (result.getConfidence() > 0.7) {
// Medium confidence: Suggest assignment, require human approval
suggestAssignment(result.getTeam(), result.getReasoning());
} else {
// Low confidence: Route to human triage
escalateToHumanTriage(bugDescription, result.getReasoning());
}
return buildResponse(result);
}
}1. Idempotency
// Always check if work was already done
if (ticketAlreadyCreated(eventId)) {
logger.info("Ticket already exists for event {}", eventId);
return existingTicket;
}2. Retry Logic with Exponential Backoff
# application.yml
messaging:
local:
retry-attempts: 3
retry-delay-ms: 1000
max-retry-delay-ms: 30000
exponential-backoff: true3. Circuit Breaker for External APIs
@Service
public class ExternalApiClient {
@CircuitBreaker(name = "jiraApi", fallbackMethod = "fallbackCreateTicket")
public Ticket createTicket(TicketRequest request) {
return jiraClient.createIssue(request);
}
private Ticket fallbackCreateTicket(TicketRequest request, Exception e) {
logger.error("Jira API unavailable, queuing for retry", e);
queueForRetry(request);
return Ticket.pending();
}
}4. Structured Logging for Observability
logger.info("Processing webhook event",
kv("event_type", eventType),
kv("event_id", eventId),
kv("source", source),
kv("handler", this.getClass().getSimpleName())
);5. Metrics for Business Insights
@Service
public class MetricsService {
private final MeterRegistry registry;
public void recordAgentInvocation(String agentType, boolean success, long durationMs) {
registry.counter("agent.invocations",
"type", agentType,
"success", String.valueOf(success)
).increment();
registry.timer("agent.duration",
"type", agentType
).record(Duration.ofMillis(durationMs));
}
}The Spring Boot foundation means you can integrate using any pattern you're familiar with. Here are common approaches:
Build REST endpoints to receive events from external systems:
@RestController
@RequestMapping("/api/github")
public class GitHubIntegrationController {
private final MessagePublisher messagePublisher;
@PostMapping("/events")
public ResponseEntity<String> handleEvent(
@RequestHeader("X-GitHub-Event") String eventType,
@RequestBody String payload) {
// Validate and publish to message queue
messagePublisher.publish("github_event", Map.of(
"event_type", eventType,
"payload", payload,
"timestamp", Instant.now()
));
return ResponseEntity.accepted().build();
}
}Use Spring's @Scheduled to poll external systems or trigger periodic agent tasks:
@Component
public class PeriodicSecurityScanJob {
private final MessagePublisher messagePublisher;
@Scheduled(cron = "0 0 2 * * *") // Daily at 2 AM
public void runSecurityScan() {
List<Repository> repos = fetchRepositories();
repos.forEach(repo -> {
messagePublisher.publish("security_scan", Map.of(
"repository", repo.getName(),
"branch", "main",
"scan_type", "full"
));
});
}
}Consume messages from external message brokers:
@Component
public class ExternalEventListener {
private final MessagePublisher messagePublisher;
@JmsListener(destination = "external.events.queue")
public void handleExternalEvent(String message) {
// Transform external message format to internal format
Map<String, Object> event = parseExternalMessage(message);
// Route to appropriate handler
messagePublisher.publish("agent_task", event);
}
}Use Spring Data to monitor database changes:
@Service
public class DatabaseEventMonitor {
@Transactional
@EventListener
public void onDeploymentCreated(DeploymentCreatedEvent event) {
// Trigger agent to analyze deployment
messagePublisher.publish("deployment_analysis", Map.of(
"deployment_id", event.getDeploymentId(),
"environment", event.getEnvironment(),
"version", event.getVersion()
));
}
}Integrate with Kafka streams for high-throughput event processing:
@Component
public class KafkaEventConsumer {
@KafkaListener(topics = "production-errors", groupId = "agent-processor")
public void consumeErrorEvent(ErrorEvent event) {
// Process high-volume error stream
messagePublisher.publish("error_analysis", Map.of(
"error_id", event.getId(),
"stack_trace", event.getStackTrace(),
"frequency", event.getCount()
));
}
}Expose gRPC endpoints for high-performance integrations:
@GrpcService
public class AgentGrpcService extends AgentServiceGrpc.AgentServiceImplBase {
@Override
public void triggerAnalysis(AnalysisRequest request,
StreamObserver<AnalysisResponse> responseObserver) {
// Handle gRPC request
messagePublisher.publish("grpc_analysis", Map.of(
"request_id", request.getId(),
"data", request.getData()
));
responseObserver.onNext(AnalysisResponse.newBuilder()
.setStatus("ACCEPTED")
.build());
responseObserver.onCompleted();
}
}Here's a full example integrating with GitHub:
Step 1: Create REST Controller
@RestController
@RequestMapping("/api/github")
public class GitHubIntegrationController {
private final MessagePublisher messagePublisher;
private final GitHubWebhookValidator validator;
@PostMapping("/events")
public ResponseEntity<String> handleEvent(
@RequestHeader("X-Hub-Signature-256") String signature,
@RequestHeader("X-GitHub-Event") String eventType,
@RequestBody String payload) {
if (!validator.isValid(payload, signature)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
messagePublisher.publish("github_event", Map.of(
"event_type", eventType,
"payload", payload,
"timestamp", Instant.now()
));
return ResponseEntity.accepted().build();
}
}Step 2: Create Handler
@Service("githubPrHandler" + Handler.HANDLER_SUFFIX)
@Scope("prototype")
public class GitHubPrHandler extends BaseHandler {
private final GitHubApiClient githubClient;
@Override
public String type() {
return "github_pr_review";
}
@Override
public Map<String, Object> handle(Map<String, Object> message) {
String prUrl = (String) message.get("pull_request_url");
// Invoke agent for code review
AgentResponse review = invokeAgent("code_review", prUrl);
// Post review comments
githubClient.postReview(prUrl, review.getComments());
return Map.of(
"status", "completed",
"comments_posted", review.getComments().size()
);
}
}Step 3: Add Configuration
@Configuration
@ConfigurationProperties(prefix = "github")
public class GitHubProperties {
private String apiToken;
private String webhookSecret;
private String baseUrl = "https://api.github.com";
// getters and setters
}Step 4: Configure in application.yml
github:
api-token: ${GITHUB_API_TOKEN}
webhook-secret: ${GITHUB_WEBHOOK_SECRET}
base-url: https://api.github.comStep 5: Add Tests
@SpringBootTest
@AutoConfigureMockMvc
class GitHubIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldAcceptValidGitHubWebhook() throws Exception {
String payload = loadTestPayload("pr_opened.json");
String signature = generateSignature(payload);
mockMvc.perform(post("/api/github/events")
.header("X-Hub-Signature-256", signature)
.header("X-GitHub-Event", "pull_request")
.content(payload))
.andExpect(status().isAccepted());
}
}Handlers process messages from the Agent Core framework:
import com.davidparry.agent.core.api.Handler;
import com.davidparry.agent.core.service.BaseHandler;
import com.davidparry.agent.core.service.MessagePublisher;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service("myCustomHandler" + Handler.HANDLER_SUFFIX)
@Scope("prototype")
public class MyCustomHandler extends BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(MyCustomHandler.class);
public MyCustomHandler(MessagePublisher messagePublisher, ObjectMapper objectMapper) {
super(messagePublisher, objectMapper);
}
@Override
public String type() {
return "my_custom_type";
}
@Override
public Map<String, Object> handle(Map<String, Object> map) {
// Custom handling logic
logger.info("Processing custom message: {}", map);
return map;
}
}Key Classes from Agent Core:
com.davidparry.agent.core.service.BaseHandler- Base class for all message handlerscom.davidparry.agent.core.service.MessagePublisher- Publishes messages to message queuescom.davidparry.agent.core.api.Handler- Handler interface and constants
Add REST endpoints for webhooks or APIs:
@RestController
@RequestMapping("/api/custom")
public class MyCustomController {
@PostMapping("/webhook")
public ResponseEntity<String> handleWebhook(@RequestBody String payload) {
// Handle webhook
return ResponseEntity.ok("OK");
}
}@Configuration
@ConfigurationProperties(prefix = "myapp")
public class MyAppProperties {
private String customProperty;
// getters and setters
}The application includes an optimized Docker build system with 85-90% faster build times for development.
cd docker
# First time: Build base image (5-10 minutes, one-time)
make base
# Build application (30-60 seconds)
make fast
# Start all services
make run- qodo-app - Main application
- activemq - Message broker for async processing
- prometheus - Metrics collection
- grafana - Metrics visualization dashboard
make help # Show all available commands
make fast # Fast development build (recommended)
make run # Start all services
make stop # Stop all services
make restart # Rebuild and restart
make logs # View application logs
make health # Check application health
make clean # Clean up images-
Copy the example environment file:
cp docker/.env.example docker/.env
-
Edit
docker/.envwith your configuration:# Required JIRA_WEBHOOK_SECRET=your-secret-here # Optional JIRA_WEBHOOK_VALIDATION_ENABLED=true MESSAGING_PROVIDER=activemq
-
Start the application:
make run
See docker/README.md for detailed Docker documentation.
# Make code changes
vim src/main/java/ai/qodo/app/handlers/MyHandler.java
# Build and test
./gradlew build
./gradlew test
# Run locally
./gradlew bootRun# Make code changes
vim src/main/java/ai/qodo/app/handlers/MyHandler.java
# Fast rebuild and restart (30-60 seconds)
cd docker
make restart
# View logs
make logsWhen a new version of Agent Core is released:
-
Update
gradle.properties:internalCoreVersion=X.Y.Z -
Rebuild with fresh dependencies:
./gradlew build --refresh-dependencies
-
Rebuild Docker image:
cd docker make fast -
Test and deploy
# Application health
curl http://localhost:8081/actuator/health
# Quick health check with Docker
cd docker && make health# All available metrics
curl http://localhost:8081/actuator/metrics
# Specific metric
curl http://localhost:8081/actuator/metrics/jvm.memory.used
# Prometheus format
curl http://localhost:8081/actuator/prometheusAccess the monitoring dashboard at: http://localhost:3000
- Username:
admin - Password:
admin
The dashboard includes:
- JVM metrics (memory, threads, GC)
- HTTP request metrics
- Message queue statistics
- Custom application metrics
# View application logs
cd docker && make logs
# Follow logs in real-time
docker compose -f docker/docker-compose.yml logs -f qodo-app
# View specific service logs
docker compose -f docker/docker-compose.yml logs activemqCheck logs:
cd docker && make logsCommon issues:
- Missing environment variables - check
docker/.env - Port conflicts - ensure ports 8081, 61616, 9090, 3000 are available
- ActiveMQ not ready - wait 30 seconds for broker to start
Symptom: Application fails to start with timeout error
Solution:
-
Increase timeout in
docker/.env:QODO_MCP_REQUEST_TIMEOUT_SECONDS=600
-
Restart:
cd docker && make restart
Gradle build fails:
# Clean and rebuild
./gradlew clean build
# Force refresh dependencies
./gradlew build --refresh-dependenciesDocker build fails:
cd docker
# Clean everything
make clean-all
# Rebuild from scratch
make base
make fastCheck webhook endpoint:
curl -X POST http://localhost:8081/api/jira/webhook \
-H "Content-Type: application/json" \
-d '{"test": "data"}'Verify Jira webhook configuration:
- URL should point to:
http://your-server:8081/api/jira/webhook - Secret must match
JIRA_WEBHOOK_SECRETenvironment variable - Signature validation enabled if
JIRA_WEBHOOK_VALIDATION_ENABLED=true
Check ActiveMQ console:
- URL: http://localhost:8161
- Username:
admin - Password:
admin
Verify queues exist:
audit- Audit messagesevent- Event messagesresponse- Response messages
Check resource usage:
docker statsIncrease JVM memory:
Edit docker/docker-compose.yml:
environment:
JAVA_OPTS: "-Xmx2g -Xms1g"POST /api/jira/webhook
Receives Jira webhook events (issue created, updated, etc.)
Headers:
Content-Type: application/jsonX-Hub-Signature(if validation enabled)
Example:
curl -X POST http://localhost:8081/api/jira/webhook \
-H "Content-Type: application/json" \
-d @jira-event.jsonPOST /api/cloudwatch/webhook
Receives AWS CloudWatch log events
Example:
curl -X POST http://localhost:8081/api/cloudwatch/webhook \
-H "Content-Type: application/json" \
-d @cloudwatch-event.jsonGET /actuator/health
Returns application health status
GET /actuator/metrics
Returns available metrics
GET /actuator/prometheus
Returns metrics in Prometheus format
Webhook → Controller → Message Queue → Handler → Agent → Response Queue
- Webhook Controller receives HTTP POST from external system
- Validation checks signature and payload
- Message Queue stores event for async processing
- Handler processes message and invokes appropriate agent
- Agent performs task (code generation, analysis, etc.)
- Response Queue stores result for delivery
- Controllers - REST endpoints for receiving webhooks
- Handlers - Process messages and orchestrate agents
- Message Broker - ActiveMQ for async message processing
- Agent Core - Framework for agent lifecycle and communication
- MCP Server - Model Context Protocol tools and capabilities
AGPL-3.0
- Agent Core - Core framework for agent orchestration
- MCP Internal - Model Context Protocol server implementation