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.
This commit is contained in:
Lucio Lelii 2026-09-02 13:32:06 +02:00
parent 7f57b8683a
commit ee695b2f5f
4 changed files with 25 additions and 32 deletions

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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"));