diff --git a/src/main/java/com/olympus/hermione/config/ChromaWarmupService.java b/src/main/java/com/olympus/hermione/config/ChromaWarmupService.java index c0f9812..898e8c2 100644 --- a/src/main/java/com/olympus/hermione/config/ChromaWarmupService.java +++ b/src/main/java/com/olympus/hermione/config/ChromaWarmupService.java @@ -5,20 +5,24 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import java.time.Duration; +import java.util.concurrent.TimeUnit; /** * Service per mantenere Chroma "sveglio" ed evitare cold start - * Esegue un warmup all'avvio e periodicamente ogni 5 minuti + * Esegue un warmup all'avvio con retry e periodicamente ogni 5 minuti */ @Component public class ChromaWarmupService { private static final Logger logger = LoggerFactory.getLogger(ChromaWarmupService.class); + private static final int MAX_RETRY_ATTEMPTS = 5; + private static final int INITIAL_BACKOFF_MS = 2000; @Value("${spring.ai.vectorstore.chroma.client.host}") private String chromaHost; @@ -26,47 +30,118 @@ public class ChromaWarmupService { @Value("${spring.ai.vectorstore.chroma.client.port}") private String chromaPort; + @Value("${spring.ai.vectorstore.chroma.collection-name:olympus_collection}") + private String collectionName; + private final RestTemplate restTemplate; + private volatile boolean chromaReady = false; public ChromaWarmupService() { this.restTemplate = new RestTemplate(); - // Timeout generosi per il warmup + // Timeout più lunghi per gestire cold start this.restTemplate.setRequestFactory(new org.springframework.http.client.SimpleClientHttpRequestFactory() {{ - setConnectTimeout((int) Duration.ofSeconds(30).toMillis()); - setReadTimeout((int) Duration.ofSeconds(60).toMillis()); + setConnectTimeout((int) Duration.ofSeconds(60).toMillis()); + setReadTimeout((int) Duration.ofMinutes(2).toMillis()); }}); } /** - * Esegue il warmup all'avvio dell'applicazione + * Esegue il warmup all'avvio dell'applicazione con retry + * Eseguito in modo asincrono per non bloccare lo startup */ + @Async @EventListener(ApplicationReadyEvent.class) public void warmupOnStartup() { - logger.info("Starting Chroma warmup on application startup..."); - performWarmup(); + logger.info("Starting aggressive Chroma warmup on application startup..."); + + // Primo warmup con retry aggressivo + for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) { + try { + logger.info("Warmup attempt {}/{}", attempt, MAX_RETRY_ATTEMPTS); + + if (performCompleteWarmup()) { + chromaReady = true; + logger.info("Chroma is now ready after {} attempt(s)", attempt); + + // Esegui ancora 2-3 warmup aggiuntivi per stabilizzare + for (int i = 1; i <= 3; i++) { + TimeUnit.SECONDS.sleep(5); + logger.info("Stabilization warmup {}/3", i); + performCompleteWarmup(); + } + return; + } + } catch (Exception e) { + logger.warn("Warmup attempt {} failed: {}", attempt, e.getMessage()); + } + + // Backoff esponenziale + if (attempt < MAX_RETRY_ATTEMPTS) { + try { + int backoffTime = INITIAL_BACKOFF_MS * (int) Math.pow(2, attempt - 1); + logger.info("Waiting {}ms before next attempt...", backoffTime); + TimeUnit.MILLISECONDS.sleep(backoffTime); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + } + } + + logger.error("Failed to warmup Chroma after {} attempts", MAX_RETRY_ATTEMPTS); } /** * Esegue il warmup ogni 5 minuti per mantenere Chroma attivo */ - @Scheduled(fixedRate = 300000) // TEST: 30 secondi (cambiare a 300000 per production) + @Scheduled(fixedRate = 300000) // 5 minuti public void scheduledWarmup() { - logger.info("Performing scheduled Chroma warmup..."); - performWarmup(); - } - - private void performWarmup() { - String healthUrl = chromaHost + ":" + chromaPort + "/api/v2/heartbeat"; - - try { - long startTime = System.currentTimeMillis(); - restTemplate.getForObject(healthUrl, String.class); - long duration = System.currentTimeMillis() - startTime; - - logger.info("Chroma warmup successful ({}ms)", duration); - } catch (Exception e) { - logger.warn("Chroma warmup failed: {}", e.getMessage()); - // Non blocchiamo l'applicazione se il warmup fallisce + if (chromaReady) { + logger.debug("Performing scheduled Chroma warmup..."); + performCompleteWarmup(); } } + + /** + * Esegue un warmup completo: heartbeat + operazioni sulla collezione + */ + private boolean performCompleteWarmup() { + long startTime = System.currentTimeMillis(); + boolean success = true; + + try { + // 1. Health check + String healthUrl = chromaHost + ":" + chromaPort + "/api/v1/heartbeat"; + restTemplate.getForObject(healthUrl, String.class); + logger.debug("Heartbeat successful"); + + // 2. Lista collezioni (operazione più pesante) + String collectionsUrl = chromaHost + ":" + chromaPort + "/api/v1/collections"; + restTemplate.getForObject(collectionsUrl, String.class); + logger.debug("Collections list successful"); + + // 3. Prova a contare elementi nella collezione principale + try { + String countUrl = chromaHost + ":" + chromaPort + "/api/v1/collections/" + collectionName + "/count"; + restTemplate.getForObject(countUrl, String.class); + logger.debug("Collection count successful"); + } catch (Exception e) { + // La collezione potrebbe non esistere ancora + logger.debug("Collection count failed (collection may not exist): {}", e.getMessage()); + } + + long duration = System.currentTimeMillis() - startTime; + logger.info("Chroma warmup completed successfully ({}ms)", duration); + + } catch (Exception e) { + logger.warn("Chroma warmup failed: {}", e.getMessage()); + success = false; + } + + return success; + } + + public boolean isChromaReady() { + return chromaReady; + } } diff --git a/src/main/java/com/olympus/hermione/services/ScenarioExecutionService.java b/src/main/java/com/olympus/hermione/services/ScenarioExecutionService.java index 7b5d914..44412f0 100644 --- a/src/main/java/com/olympus/hermione/services/ScenarioExecutionService.java +++ b/src/main/java/com/olympus/hermione/services/ScenarioExecutionService.java @@ -67,11 +67,10 @@ import org.bson.types.ObjectId; import org.neo4j.driver.Driver; import org.slf4j.Logger; -import com.azure.ai.openai.OpenAIClient; import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.core.credential.AzureKeyCredential; -import com.olympus.hermione.stepSolvers.ExternalAgentSolver; import com.olympus.hermione.stepSolvers.ExternalCodeGenieSolver; +import com.olympus.hermione.stepSolvers.OlympusAgentSolver; import com.olympus.hermione.stepSolvers.OlynmpusChatClientSolver; @Service @@ -265,12 +264,12 @@ public class ScenarioExecutionService { case "RAG_SOURCE_CODE": solver = new SourceCodeRagSolver(); break; - case "EXTERNAL_AGENT": - solver = new ExternalAgentSolver(); - break; case "EXTERNAL_CODEGENIE": solver = new ExternalCodeGenieSolver(); break; + case "OLYMPUS_AGENT": + solver = new OlympusAgentSolver(); + break; case "SUMMARIZE_DOC": solver = new SummarizeDocSolver(); break; @@ -291,6 +290,9 @@ public class ScenarioExecutionService { logger.info("Solving step: " + step.getStepId()); scenarioExecution.setCurrentStepId(step.getStepId()); scenarioExecution.setCurrentStepDescription(step.getName()); + + // Save immediately so frontend can show correct current step + scenarioExecutionRepository.save(scenarioExecution); ScenarioExecution scenarioExecutionNew = scenarioExecution; diff --git a/src/main/java/com/olympus/hermione/stepSolvers/OlympusAgentSolver.java b/src/main/java/com/olympus/hermione/stepSolvers/OlympusAgentSolver.java new file mode 100644 index 0000000..bf17cdb --- /dev/null +++ b/src/main/java/com/olympus/hermione/stepSolvers/OlympusAgentSolver.java @@ -0,0 +1,164 @@ +package com.olympus.hermione.stepSolvers; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import com.olympus.hermione.models.ScenarioExecution; +import com.olympus.hermione.utility.AttributeParser; + +import ch.qos.logback.classic.Logger; + +public class OlympusAgentSolver extends StepSolver { + + private String agent_id; + private String agent_message; + private String agent_input; + private String agent_context; + private String agent_base_url; + private String agent_output_variable; + + Logger logger = (Logger) LoggerFactory.getLogger(OlympusAgentSolver.class); + + private void loadParameters(){ + logger.info("Loading parameters for LangGraph Agent"); + + // Agent ID (required) + if(this.step.getAttributes().get("agent_id") != null){ + this.agent_id = (String) this.step.getAttributes().get("agent_id"); + logger.info("agent_id: " + this.agent_id); + } else { + throw new IllegalArgumentException("agent_id is required"); + } + + // Message/Input + if(this.step.getAttributes().get("agent_message") != null){ + this.agent_message = (String) this.step.getAttributes().get("agent_message"); + logger.info("agent_message: " + this.agent_message); + } + + // Agent Input + if(this.step.getAttributes().get("agent_input") != null){ + this.agent_input = (String) this.step.getAttributes().get("agent_input"); + logger.info("agent_input: " + this.agent_input); + } + + // Context (optional JSON) + if(this.step.getAttributes().get("agent_context") != null){ + this.agent_context = (String) this.step.getAttributes().get("agent_context"); + logger.info("agent_context: " + this.agent_context); + } + + // Base URL (default to localhost) + if(this.step.getAttributes().get("agent_base_url") != null){ + this.agent_base_url = (String) this.step.getAttributes().get("agent_base_url"); + } + logger.info("agent_base_url: " + this.agent_base_url); + + // Output variable name + if(this.step.getAttributes().get("agent_output_variable") != null){ + this.agent_output_variable = (String) this.step.getAttributes().get("agent_output_variable"); + } + logger.info("agent_output_variable: " + this.agent_output_variable); + + + // Parse variables from execution context + AttributeParser attributeParser = new AttributeParser(this.scenarioExecution); + + if(this.agent_message != null){ + this.agent_message = attributeParser.parse(this.agent_message); + } + if(this.agent_input != null){ + this.agent_input = attributeParser.parse(this.agent_input); + } + if(this.agent_context != null){ + this.agent_context = attributeParser.parse(this.agent_context); + } + } + + @Override + public ScenarioExecution solveStep() throws Exception { + + logger.info("Solving LangGraph Agent step: " + this.step.getName()); + + this.scenarioExecution.setCurrentStepId(this.step.getStepId()); + + loadParameters(); + + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + String endpoint; + JSONObject requestBody = new JSONObject(); + + endpoint = this.agent_base_url + "/agent/" + this.agent_id + "/execute"; + + // Add agent input (required) + String inputValue = this.agent_input != null ? this.agent_input : + (this.agent_message != null ? this.agent_message : ""); + requestBody.put("agent_input", inputValue); + + // Add context if provided + if(this.agent_context != null && !this.agent_context.isEmpty()){ + try { + JSONObject contextObj = new JSONObject(this.agent_context); + requestBody.put("context", contextObj); + } catch (Exception e) { + logger.warn("Failed to parse agent_context as JSON, using empty object", e); + requestBody.put("context", new JSONObject()); + } + } else { + requestBody.put("context", new JSONObject()); + } + + // Add scenario ID from execution + requestBody.put("scenario_id", this.scenarioExecution.getScenario().getId()); + + + logger.info("Calling LangGraph Agent endpoint: " + endpoint); + logger.info("Request body: " + requestBody.toString()); + + HttpEntity request = new HttpEntity<>(requestBody.toString(), headers); + + try { + ResponseEntity response = restTemplate.exchange( + endpoint, + HttpMethod.POST, + request, + String.class + ); + + JSONObject jsonResponse = new JSONObject(response.getBody()); + + logger.info("Hermione execution completed"); + logger.info("Execution ID: " + jsonResponse.optString("execution_id")); + logger.info("Final output: " + jsonResponse.optString("final_output")); + + // Store the complete response + this.scenarioExecution.getExecSharedMap().put(this.agent_output_variable, jsonResponse.toString()); + + // Also store final output separately for easy access + this.scenarioExecution.getExecSharedMap().put( + this.agent_output_variable + "_final_output", + jsonResponse.optString("final_output", "") + ); + + + // Move to next step + this.scenarioExecution.setNextStepId(this.step.getNextStepId()); + + } catch (Exception e) { + logger.error("Error calling LangGraph Agent service", e); + throw new Exception("LangGraph Agent execution failed: " + e.getMessage()); + } + + return this.scenarioExecution; + } +}