mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Inform AI engine which endpoints are disabled on the backend (#6251)
# Description of Changes Have the Java send a list of enabled endpoints to the AI engine so it can intelligently respond to the user that the tool does exist but is disabled on the server so it can't acutally run the operation, instead of the current behaviour where it sends the API call back and then 503 errors because the execution fails when the URL is disabled. <img width="380" height="208" alt="image" src="https://github.com/user-attachments/assets/5842fb2e-2e55-45a5-8205-25515636daae" /> --------- Co-authored-by: EthanHealy01 <[email protected]>
This commit is contained in:
co-authored by
EthanHealy01
parent
5541dd666c
commit
51f5345151
+30
-5
@@ -34,11 +34,14 @@ import stirling.software.proprietary.model.api.ai.AiWorkflowRequest;
|
||||
import stirling.software.proprietary.model.api.ai.AiWorkflowResponse;
|
||||
import stirling.software.proprietary.model.api.ai.AiWorkflowResultFile;
|
||||
import stirling.software.proprietary.service.AiEngineClient;
|
||||
import stirling.software.proprietary.service.AiEngineEndpointResolver;
|
||||
import stirling.software.proprietary.service.AiWorkflowService;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.node.ArrayNode;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@@ -53,6 +56,7 @@ public class AiEngineController {
|
||||
private final Executor aiStreamExecutor;
|
||||
private final TaskManager taskManager;
|
||||
private final JobOwnershipService jobOwnershipService;
|
||||
private final AiEngineEndpointResolver endpointResolver;
|
||||
|
||||
/**
|
||||
* SSE emitter timeout. Long enough to accommodate multi-gigabyte PDF workflows (OCR on a
|
||||
@@ -68,13 +72,15 @@ public class AiEngineController {
|
||||
ObjectMapper objectMapper,
|
||||
@Qualifier("aiStreamExecutor") Executor aiStreamExecutor,
|
||||
TaskManager taskManager,
|
||||
JobOwnershipService jobOwnershipService) {
|
||||
JobOwnershipService jobOwnershipService,
|
||||
AiEngineEndpointResolver endpointResolver) {
|
||||
this.aiEngineClient = aiEngineClient;
|
||||
this.aiWorkflowService = aiWorkflowService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.aiStreamExecutor = aiStreamExecutor;
|
||||
this.taskManager = taskManager;
|
||||
this.jobOwnershipService = jobOwnershipService;
|
||||
this.endpointResolver = endpointResolver;
|
||||
}
|
||||
|
||||
@GetMapping("/health")
|
||||
@@ -197,17 +203,36 @@ public class AiEngineController {
|
||||
"Sends a user message to the PDF edit agent which returns a structured plan"
|
||||
+ " of tool operations to perform")
|
||||
public ResponseEntity<String> pdfEdit(@RequestBody String requestBody) throws IOException {
|
||||
validateJson(requestBody);
|
||||
String response = aiEngineClient.post("/api/v1/pdf/edit", requestBody);
|
||||
JsonNode parsed = parseJson(requestBody);
|
||||
if (!parsed.isObject()) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "Request body must be a JSON object");
|
||||
}
|
||||
String forwardedBody = withEnabledEndpoints((ObjectNode) parsed);
|
||||
String response = aiEngineClient.post("/api/v1/pdf/edit", forwardedBody);
|
||||
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(response);
|
||||
}
|
||||
|
||||
private void validateJson(String body) {
|
||||
private JsonNode parseJson(String body) {
|
||||
try {
|
||||
objectMapper.readValue(body, JsonNode.class);
|
||||
return objectMapper.readValue(body, JsonNode.class);
|
||||
} catch (JacksonException e) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "Request body is not valid JSON");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Always overwrite {@code enabled_endpoints} with the server's view of which endpoints are
|
||||
* usable. The engine must not trust a client-supplied list - the gate is owned by the Java
|
||||
* EndpointConfiguration. Values are full URL paths (e.g. {@code /api/v1/misc/compress-pdf})
|
||||
* that the engine matches against its {@code ToolEndpoint} enum, silently dropping any it
|
||||
* doesn't recognise (which lets the two sides drift in either direction without breaking).
|
||||
*/
|
||||
private String withEnabledEndpoints(ObjectNode body) {
|
||||
ArrayNode enabled = objectMapper.createArrayNode();
|
||||
endpointResolver.getEnabledEndpointUrls().forEach(enabled::add);
|
||||
body.set("enabled_endpoints", enabled);
|
||||
return body.toString();
|
||||
}
|
||||
}
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package stirling.software.proprietary.service;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
|
||||
/**
|
||||
* Discovers every {@code /api/v1/...} request mapping in the application and exposes the subset
|
||||
* that {@link EndpointConfiguration} reports as currently enabled. The AI engine receives this list
|
||||
* as-is and silently drops anything it doesn't recognise, so we don't try to predict what the
|
||||
* engine considers a tool - we just emit what's enabled here.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AiEngineEndpointResolver {
|
||||
|
||||
private static final String API_PREFIX = "/api/v1/";
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
// Written once on the Spring startup thread during ContextRefreshedEvent, read on HTTP
|
||||
// request threads. Spring's lifecycle establishes happens-before (the servlet container
|
||||
// and its worker threads are started after refresh completes), so no volatile is needed.
|
||||
private Set<String> apiUrls = Set.of();
|
||||
|
||||
public AiEngineEndpointResolver(
|
||||
ApplicationContext applicationContext, EndpointConfiguration endpointConfiguration) {
|
||||
this.applicationContext = applicationContext;
|
||||
this.endpointConfiguration = endpointConfiguration;
|
||||
}
|
||||
|
||||
@EventListener(ContextRefreshedEvent.class)
|
||||
public void discoverApiUrls() {
|
||||
Set<String> discovered = new TreeSet<>();
|
||||
for (RequestMappingHandlerMapping mapping :
|
||||
applicationContext.getBeansOfType(RequestMappingHandlerMapping.class).values()) {
|
||||
for (RequestMappingInfo info : mapping.getHandlerMethods().keySet()) {
|
||||
for (String pattern : extractPatterns(info)) {
|
||||
if (pattern.startsWith(API_PREFIX)) {
|
||||
discovered.add(pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
apiUrls = Set.copyOf(discovered);
|
||||
log.debug("Discovered {} /api/v1/ endpoint URLs for AI engine filtering", apiUrls.size());
|
||||
}
|
||||
|
||||
public List<String> getEnabledEndpointUrls() {
|
||||
return apiUrls.stream()
|
||||
.filter(endpointConfiguration::isEndpointEnabledForUri)
|
||||
.sorted()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static Set<String> extractPatterns(RequestMappingInfo info) {
|
||||
try {
|
||||
Method getDirectPaths = info.getClass().getMethod("getDirectPaths");
|
||||
Object result = getDirectPaths.invoke(info);
|
||||
if (result instanceof Set<?> set) {
|
||||
Set<String> patterns = new HashSet<>();
|
||||
for (Object value : set) {
|
||||
if (value instanceof String s) {
|
||||
patterns.add(s);
|
||||
}
|
||||
}
|
||||
return patterns;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.trace("getDirectPaths unavailable on RequestMappingInfo", e);
|
||||
}
|
||||
return Set.of();
|
||||
}
|
||||
}
|
||||
+4
@@ -71,6 +71,7 @@ public class AiWorkflowService {
|
||||
private final ToolMetadataService toolMetadataService;
|
||||
private final TempFileManager tempFileManager;
|
||||
private final FileIdStrategy fileIdStrategy;
|
||||
private final AiEngineEndpointResolver endpointResolver;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ProgressListener {
|
||||
@@ -125,6 +126,7 @@ public class AiWorkflowService {
|
||||
request.getConversationHistory() == null
|
||||
? new ArrayList<>()
|
||||
: new ArrayList<>(request.getConversationHistory()));
|
||||
initialRequest.setEnabledEndpoints(endpointResolver.getEnabledEndpointUrls());
|
||||
|
||||
listener.onProgress(AiWorkflowProgressEvent.of(AiWorkflowPhase.ANALYZING));
|
||||
|
||||
@@ -219,6 +221,7 @@ public class AiWorkflowService {
|
||||
nextRequest.setConversationHistory(request.getConversationHistory());
|
||||
nextRequest.setArtifacts(pdfContentExtractor.buildArtifacts(contentResults));
|
||||
nextRequest.setResumeWith(response.getResumeWith());
|
||||
nextRequest.setEnabledEndpoints(request.getEnabledEndpoints());
|
||||
return new WorkflowState.Pending(nextRequest);
|
||||
} finally {
|
||||
for (LoadedFile lf : loadedFiles) {
|
||||
@@ -624,5 +627,6 @@ public class AiWorkflowService {
|
||||
private List<AiConversationMessage> conversationHistory = new ArrayList<>();
|
||||
private List<WorkflowArtifact> artifacts = new ArrayList<>();
|
||||
private String resumeWith;
|
||||
private List<String> enabledEndpoints = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -79,6 +79,7 @@ class AiWorkflowServiceTest {
|
||||
@Mock private FileStorage fileStorage;
|
||||
@Mock private ToolMetadataService toolMetadataService;
|
||||
@Mock private FileIdStrategy fileIdStrategy;
|
||||
@Mock private AiEngineEndpointResolver endpointResolver;
|
||||
|
||||
@TempDir Path tempDir;
|
||||
|
||||
@@ -110,7 +111,9 @@ class AiWorkflowServiceTest {
|
||||
fileStorage,
|
||||
toolMetadataService,
|
||||
tempFileManager,
|
||||
fileIdStrategy);
|
||||
fileIdStrategy,
|
||||
endpointResolver);
|
||||
when(endpointResolver.getEnabledEndpointUrls()).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user