ProjectAutopilotService.java
package com.taxonomy.portfolio.service;
import com.taxonomy.portfolio.dto.CopilotDtos.AiAutomationStatus;
import com.taxonomy.portfolio.dto.CopilotDtos.CopilotOperationView;
import com.taxonomy.portfolio.dto.CopilotDtos.ProjectAutopilotRunRequest;
import com.taxonomy.portfolio.dto.CopilotDtos.ProjectAutopilotRunView;
import com.taxonomy.portfolio.dto.CopilotDtos.ProjectAutopilotStatus;
import com.taxonomy.portfolio.dto.PortfolioDtos.RequirementView;
import com.taxonomy.workspace.service.WorkspaceContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/** Bounded, exact-tenant project-level dispatch for explicitly enabled Autopilot. */
@Service
public class ProjectAutopilotService {
private static final int MAX_CONFIGURED_BATCH = 500;
private final CopilotAutomationService automationService;
private final ProjectPortfolioService projectService;
private final int maximumBatchRequirements;
public ProjectAutopilotService(
CopilotAutomationService automationService,
ProjectPortfolioService projectService,
@Value("${taxonomy.ai.autopilot.max-project-requirements:50}")
int maximumBatchRequirements) {
this.automationService = automationService;
this.projectService = projectService;
if (maximumBatchRequirements < 1
|| maximumBatchRequirements > MAX_CONFIGURED_BATCH) {
throw new IllegalArgumentException(
"taxonomy.ai.autopilot.max-project-requirements must be between 1 and "
+ MAX_CONFIGURED_BATCH);
}
this.maximumBatchRequirements = maximumBatchRequirements;
}
public ProjectAutopilotStatus status(
Long projectId,
String username,
WorkspaceContext context) {
List<RequirementView> requirements = projectService.listRequirements(
projectId, username, context);
AiAutomationStatus automation = automationService.status();
return new ProjectAutopilotStatus(
projectId,
automation.autopilotReady(),
automation.runAfterRequirementSave(),
requirements.size(),
maximumBatchRequirements,
automation.reason());
}
public ProjectAutopilotRunView run(
Long projectId,
ProjectAutopilotRunRequest request,
String username,
WorkspaceContext context) {
AiAutomationStatus automation = automationService.status();
if (!automation.autopilotReady()) {
throw PortfolioException.validation(automation.reason());
}
List<RequirementView> requirements = projectService.listRequirements(
projectId, username, context);
List<RequirementView> selected = selectRequirements(requirements, request);
int requestedLimit = requestedLimit(request);
if (selected.size() > requestedLimit) {
throw PortfolioException.validation(
"Project Autopilot selected " + selected.size()
+ " requirements, exceeding the bounded batch limit of "
+ requestedLimit
+ ". Supply explicit requirementIds or increase the operator limit.");
}
List<String> operationIds = new ArrayList<>(selected.size());
boolean complete = true;
for (RequirementView requirement : selected) {
Optional<CopilotOperationView> operation = automationService.tryAutopilot(
projectId,
requirement.id(),
username,
context);
String operationId = operation
.map(CopilotOperationView::operationId)
.orElse(null);
if (operationId == null) {
complete = false;
break;
}
operationIds.add(operationId);
}
String message;
if (complete) {
message = "Autopilot operations were persisted; every generated decision remains subject to human review.";
} else {
message = "Autopilot became unavailable after "
+ operationIds.size()
+ " of "
+ selected.size()
+ " operations were persisted; already-started operations remain active, "
+ "and the remaining requirements can be submitted again. Every generated "
+ "decision remains subject to human review.";
}
return new ProjectAutopilotRunView(
projectId,
selected.size(),
operationIds.size(),
List.copyOf(operationIds),
message);
}
private List<RequirementView> selectRequirements(
List<RequirementView> requirements,
ProjectAutopilotRunRequest request) {
if (request == null
|| request.requirementIds() == null
|| request.requirementIds().isEmpty()) {
return requirements;
}
Set<Long> requestedIds = new LinkedHashSet<>();
for (Long id : request.requirementIds()) {
if (id == null) {
throw PortfolioException.validation(
"Project Autopilot requirementIds must not contain null");
}
requestedIds.add(id);
}
Map<Long, RequirementView> byId = new LinkedHashMap<>();
for (RequirementView requirement : requirements) {
byId.put(requirement.id(), requirement);
}
List<RequirementView> selected = new ArrayList<>(requestedIds.size());
for (Long id : requestedIds) {
RequirementView requirement = byId.get(id);
if (requirement == null) {
throw PortfolioException.notFound(
"Requirement " + id + " was not found in project Autopilot scope");
}
selected.add(requirement);
}
return List.copyOf(selected);
}
private int requestedLimit(ProjectAutopilotRunRequest request) {
if (request == null || request.maxRequirements() == null) {
return maximumBatchRequirements;
}
int requested = request.maxRequirements();
if (requested < 1 || requested > maximumBatchRequirements) {
throw PortfolioException.validation(
"maxRequirements must be between 1 and the operator limit of "
+ maximumBatchRequirements);
}
return requested;
}
}