From ee695b2f5fc0ce5aae903ce66bfdd382beebef01 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Wed, 2 Sep 2026 13:32:06 +0200 Subject: [PATCH] Finalize async-only assistant flow endpoints, sync client flow edits into session Removes the now-unused synchronous /flows/draft, /flows/refine and /flows/fix mappings in favor of the session-based submitMessage + polling flow, and lets submitMessage accept an optional flow snapshot so manual canvas edits made outside the chat are reflected before the assistant acts on the next message. --- .../AssistantConversationService.java | 3 ++ .../model/AssistantSessionMessageRequest.java | 8 ++++- .../controllers/AssistantController.java | 34 ++++--------------- .../controllers/AssistantControllerTest.java | 12 ++++--- 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/src/main/java/it/cnr/isti/workflow/manager/assistant/AssistantConversationService.java b/src/main/java/it/cnr/isti/workflow/manager/assistant/AssistantConversationService.java index 8c5c372..3d21024 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/assistant/AssistantConversationService.java +++ b/src/main/java/it/cnr/isti/workflow/manager/assistant/AssistantConversationService.java @@ -101,6 +101,9 @@ public class AssistantConversationService { throw new ResponseStatusException(HttpStatus.CONFLICT, "A call is already in progress for this session"); } + if (request.flow() != null) { + session.currentFlow = request.flow(); + } session.appendMessage(AssistantMessageRole.USER, request.message(), null); Instant now = Instant.now(); diff --git a/src/main/java/it/cnr/isti/workflow/manager/assistant/model/AssistantSessionMessageRequest.java b/src/main/java/it/cnr/isti/workflow/manager/assistant/model/AssistantSessionMessageRequest.java index 2f7a181..180bef0 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/assistant/model/AssistantSessionMessageRequest.java +++ b/src/main/java/it/cnr/isti/workflow/manager/assistant/model/AssistantSessionMessageRequest.java @@ -1,6 +1,12 @@ package it.cnr.isti.workflow.manager.assistant.model; +import it.cnr.isti.workflow.manager.flows.model.FlowCreateRequest; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; -public record AssistantSessionMessageRequest(@NotBlank String message) { +public record AssistantSessionMessageRequest(@NotBlank String message, @Valid FlowCreateRequest flow) { + + public AssistantSessionMessageRequest(String message) { + this(message, null); + } } diff --git a/src/main/java/it/cnr/isti/workflow/manager/controllers/AssistantController.java b/src/main/java/it/cnr/isti/workflow/manager/controllers/AssistantController.java index fa4b657..357b858 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/controllers/AssistantController.java +++ b/src/main/java/it/cnr/isti/workflow/manager/controllers/AssistantController.java @@ -61,45 +61,25 @@ public class AssistantController { @Value("${app.assistant.default-repair-model:${app.assistant.default-model}}") private String defaultRepairModel; - /** Convenience entry point for in-process callers; HTTP requests use the authenticated overload. */ + /** + * Test-only entry point. Generation is exposed to real clients exclusively through the + * async session flow (POST /sessions/{id}/messages + GET /calls/{callId}) so the caller + * gets progress updates instead of blocking on a multi-minute request. + */ public AssistantFlowResponse draft(AssistantGenerationRequest request) { return flowAssistantService.draft(request); } - @PostMapping("/flows/draft") - @SecurityRequirement(name = "bearerAuth") - @Operation(summary = "Generate a flow draft", description = "Creates a flow draft from a natural language request.") - public AssistantFlowResponse draftForUser(@RequestBody @Valid AssistantGenerationRequest request, - @AuthenticationPrincipal LoginEntity userDetails) { - return flowAssistantService.draft(request, userDetails.getUsername()); - } - - /** Convenience entry point for in-process callers; HTTP requests use the authenticated overload. */ + /** Test-only entry point; see {@link #draft(AssistantGenerationRequest)}. */ public AssistantFlowResponse refine(AssistantRefineRequest request) { return flowAssistantService.refine(request); } - @PostMapping("/flows/refine") - @SecurityRequirement(name = "bearerAuth") - @Operation(summary = "Refine an existing flow", description = "Updates an existing flow definition using a natural language request.") - public AssistantFlowResponse refineForUser(@RequestBody @Valid AssistantRefineRequest request, - @AuthenticationPrincipal LoginEntity userDetails) { - return flowAssistantService.refine(request, userDetails.getUsername()); - } - - /** Convenience entry point for in-process callers; HTTP requests use the authenticated overload. */ + /** Test-only entry point; see {@link #draft(AssistantGenerationRequest)}. */ public AssistantFlowResponse fix(AssistantFixRequest request) { return flowAssistantService.fix(request); } - @PostMapping("/flows/fix") - @SecurityRequirement(name = "bearerAuth") - @Operation(summary = "Fix an invalid flow", description = "Repairs a flow using backend validation errors and optional user guidance.") - public AssistantFlowResponse fixForUser(@RequestBody @Valid AssistantFixRequest request, - @AuthenticationPrincipal LoginEntity userDetails) { - return flowAssistantService.fix(request, userDetails.getUsername()); - } - /** Convenience entry point for in-process callers; HTTP requests use the authenticated overload. */ public AssistantExplainResponse explain(AssistantExplainRequest request) { return flowAssistantService.explain(request); diff --git a/src/test/java/it/cnr/isti/workflow/manager/controllers/AssistantControllerTest.java b/src/test/java/it/cnr/isti/workflow/manager/controllers/AssistantControllerTest.java index 7ea2d08..64ec4c5 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/controllers/AssistantControllerTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/controllers/AssistantControllerTest.java @@ -24,6 +24,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.server.ResponseStatusException; import it.cnr.isti.workflow.manager.app.ObjectMapperHolder; +import it.cnr.isti.workflow.manager.assistant.FlowAssistantService; import it.cnr.isti.workflow.manager.assistant.model.AssistantCallAcceptedResponse; import it.cnr.isti.workflow.manager.assistant.model.AssistantCallStatus; import it.cnr.isti.workflow.manager.assistant.model.AssistantCallView; @@ -95,6 +96,9 @@ public class AssistantControllerTest { @Autowired private AssistantController assistantController; + @Autowired + private FlowAssistantService flowAssistantService; + @Autowired private MockMvc mockMvc; @@ -231,11 +235,11 @@ public class AssistantControllerTest { VaultSecretView credential = userSecretService.create("testuser", new VaultSecretCreateRequest("gemini assistant", "Gemini", null, "user-gemini-key")); - AssistantFlowResponse response = assistantController.draftForUser(new AssistantGenerationRequest( + AssistantFlowResponse response = flowAssistantService.draft(new AssistantGenerationRequest( "create a flow that classifies incoming tickets", 1, new AssistantLlmSelection("Gemini", MODEL, null, credential.id())), - new LoginEntity("testuser", "testpassword")); + "testuser"); assertTrue(response.valid(), () -> "Unexpected validation errors: " + response.validationErrors()); LLMBlockConfiguration configuration = (LLMBlockConfiguration) response.flow().flow().getBlocks().getFirst() @@ -252,11 +256,11 @@ public class AssistantControllerTest { Mockito.when(geminiLLMProvider.requiresAuthorization()).thenReturn(true); ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> assistantController.draftForUser(new AssistantGenerationRequest( + () -> flowAssistantService.draft(new AssistantGenerationRequest( "create a flow that classifies incoming tickets", 1, new AssistantLlmSelection("Gemini", MODEL, null, null)), - new LoginEntity("testuser", "testpassword"))); + "testuser")); assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode()); assertTrue(exception.getReason().contains("credentialId"));