Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/ai-runtime-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ Agent가 문서 작성에 요구한 값은 `***`, `OOO`로 바꾸지 않고 원
{
"candidateRef": "candidate-1",
"workerRef": "30000000-0000-0000-0000-000000000001",
"workflowId": "EXPIRY_RENEWAL",
"workflowId": "WF-STY-001",
"extractedSlots": {
"stay_expiry_date": "2026-12-31"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.fowoco.server.airun.api;

import com.fowoco.server.airun.domain.AiCandidateDecisionAction;
import jakarta.validation.constraints.NotNull;
import java.util.UUID;

public record AiCandidateDecisionItemRequest(
@NotNull UUID candidateId,
@NotNull AiCandidateDecisionAction action
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.fowoco.server.airun.api;

import com.fowoco.server.airun.application.AiCandidateDecisionResult;
import com.fowoco.server.airun.domain.AiCandidateDecisionAction;
import java.util.UUID;

public record AiCandidateDecisionItemResponse(
UUID candidateId,
AiCandidateDecisionAction action
) {
static AiCandidateDecisionItemResponse from(AiCandidateDecisionResult.Decision result) {
return new AiCandidateDecisionItemResponse(result.candidateId(), result.action());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.fowoco.server.airun.api;

import com.fowoco.server.airun.application.AiCandidateDecisionResult;
import java.util.List;
import java.util.UUID;

public record AiCandidateDecisionResponse(
UUID decisionBatchId,
UUID aiRunId,
UUID caseId,
List<UUID> taskIds,
List<AiCandidateDecisionItemResponse> decisions,
long runVersion
) {
static AiCandidateDecisionResponse from(AiCandidateDecisionResult result) {
return new AiCandidateDecisionResponse(
result.decisionBatchId(),
result.aiRunId(),
result.caseId(),
result.taskIds(),
result.decisions().stream().map(AiCandidateDecisionItemResponse::from).toList(),
result.runVersion()
);
}
}
48 changes: 48 additions & 0 deletions src/main/java/com/fowoco/server/airun/api/AiRunController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.fowoco.server.airun.api;

import com.fowoco.server.airun.application.AiCandidateDecisionCommand;
import com.fowoco.server.airun.application.AiCandidateDecisionService;
import com.fowoco.server.airun.application.AiRunService;
import com.fowoco.server.auth.application.ActorContext;
import com.fowoco.server.auth.application.port.ActorContextProvider;
Expand Down Expand Up @@ -33,13 +35,16 @@
public class AiRunController {

private final AiRunService aiRunService;
private final AiCandidateDecisionService candidateDecisionService;
private final ActorContextProvider actorContextProvider;

public AiRunController(
AiRunService aiRunService,
AiCandidateDecisionService candidateDecisionService,
ActorContextProvider actorContextProvider
) {
this.aiRunService = aiRunService;
this.candidateDecisionService = candidateDecisionService;
this.actorContextProvider = actorContextProvider;
}

Expand Down Expand Up @@ -116,6 +121,49 @@ public ResponseEntity<AiRunResponse> answer(
)));
}

@Operation(
operationId = "decideAiRunCandidates",
summary = "AI 업무 후보 채택·폐기",
description = "HR이 채택한 후보만 Case와 업무카드로 생성합니다. 승인과 발송은 별도입니다."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "후보 결정 완료"),
@ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"),
@ApiResponse(responseCode = "404", ref = "#/components/responses/NotFound"),
@ApiResponse(responseCode = "409", ref = "#/components/responses/Conflict"),
@ApiResponse(responseCode = "422", ref = "#/components/responses/UnprocessableEntity")
})
@PreAuthorize("hasAnyRole('ADMIN', 'HR')")
@PostMapping(
path = "/{aiRunId}/candidate-decisions",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public AiCandidateDecisionResponse decideCandidates(
@PathVariable UUID aiRunId,
@Parameter(description = "같은 후보 결정을 중복 생성하지 않기 위한 키", required = true)
@RequestHeader("Idempotency-Key") String idempotencyKey,
@Valid @RequestBody DecideAiRunCandidatesRequest request,
HttpServletRequest servletRequest
) {
AiCandidateDecisionCommand command = new AiCandidateDecisionCommand(
request.expectedRunVersion(),
request.decisions().stream()
.map(decision -> new AiCandidateDecisionCommand.Decision(
decision.candidateId(),
decision.action()
))
.toList()
);
return AiCandidateDecisionResponse.from(candidateDecisionService.decide(
aiRunId,
idempotencyKey,
command,
actor(),
RequestMetadata.from(servletRequest)
));
}

private ActorContext actor() {
return actorContextProvider.requireCurrentActor();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.fowoco.server.airun.api;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.List;

public record DecideAiRunCandidatesRequest(
@Min(0) long expectedRunVersion,
@NotNull @NotEmpty @Size(max = 20) List<@Valid AiCandidateDecisionItemRequest> decisions
) {
public DecideAiRunCandidatesRequest {
decisions = decisions == null ? null : List.copyOf(decisions);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.fowoco.server.airun.application;

import com.fowoco.server.airun.domain.AiCandidateDecisionAction;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

public record AiCandidateDecisionCommand(
long expectedRunVersion,
List<Decision> decisions
) {
public AiCandidateDecisionCommand {
decisions = List.copyOf(decisions);
}

public record Decision(UUID candidateId, AiCandidateDecisionAction action) {
public Decision {
Objects.requireNonNull(candidateId);
Objects.requireNonNull(action);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.fowoco.server.airun.application;

import com.fowoco.server.airun.domain.AiCandidateDecisionAction;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

public record AiCandidateDecisionResult(
UUID decisionBatchId,
UUID aiRunId,
UUID caseId,
List<UUID> taskIds,
List<Decision> decisions,
long runVersion
) {
public AiCandidateDecisionResult {
Objects.requireNonNull(decisionBatchId);
Objects.requireNonNull(aiRunId);
taskIds = List.copyOf(taskIds);
decisions = List.copyOf(decisions);
}

public record Decision(UUID candidateId, AiCandidateDecisionAction action) {
public Decision {
Objects.requireNonNull(candidateId);
Objects.requireNonNull(action);
}
}
}
Loading
Loading