Merged PR 219: Update Hermione
This commit is contained in:
@@ -5,20 +5,24 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service per mantenere Chroma "sveglio" ed evitare cold start
|
* 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
|
@Component
|
||||||
public class ChromaWarmupService {
|
public class ChromaWarmupService {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ChromaWarmupService.class);
|
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}")
|
@Value("${spring.ai.vectorstore.chroma.client.host}")
|
||||||
private String chromaHost;
|
private String chromaHost;
|
||||||
@@ -26,47 +30,118 @@ public class ChromaWarmupService {
|
|||||||
@Value("${spring.ai.vectorstore.chroma.client.port}")
|
@Value("${spring.ai.vectorstore.chroma.client.port}")
|
||||||
private String chromaPort;
|
private String chromaPort;
|
||||||
|
|
||||||
|
@Value("${spring.ai.vectorstore.chroma.collection-name:olympus_collection}")
|
||||||
|
private String collectionName;
|
||||||
|
|
||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
|
private volatile boolean chromaReady = false;
|
||||||
|
|
||||||
public ChromaWarmupService() {
|
public ChromaWarmupService() {
|
||||||
this.restTemplate = new RestTemplate();
|
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() {{
|
this.restTemplate.setRequestFactory(new org.springframework.http.client.SimpleClientHttpRequestFactory() {{
|
||||||
setConnectTimeout((int) Duration.ofSeconds(30).toMillis());
|
setConnectTimeout((int) Duration.ofSeconds(60).toMillis());
|
||||||
setReadTimeout((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)
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
public void warmupOnStartup() {
|
public void warmupOnStartup() {
|
||||||
logger.info("Starting Chroma warmup on application startup...");
|
logger.info("Starting aggressive Chroma warmup on application startup...");
|
||||||
performWarmup();
|
|
||||||
|
// 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
|
* 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() {
|
public void scheduledWarmup() {
|
||||||
logger.info("Performing scheduled Chroma warmup...");
|
if (chromaReady) {
|
||||||
performWarmup();
|
logger.debug("Performing scheduled Chroma warmup...");
|
||||||
}
|
performCompleteWarmup();
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,11 +67,10 @@ import org.bson.types.ObjectId;
|
|||||||
import org.neo4j.driver.Driver;
|
import org.neo4j.driver.Driver;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
import com.azure.ai.openai.OpenAIClient;
|
|
||||||
import com.azure.ai.openai.OpenAIClientBuilder;
|
import com.azure.ai.openai.OpenAIClientBuilder;
|
||||||
import com.azure.core.credential.AzureKeyCredential;
|
import com.azure.core.credential.AzureKeyCredential;
|
||||||
import com.olympus.hermione.stepSolvers.ExternalAgentSolver;
|
|
||||||
import com.olympus.hermione.stepSolvers.ExternalCodeGenieSolver;
|
import com.olympus.hermione.stepSolvers.ExternalCodeGenieSolver;
|
||||||
|
import com.olympus.hermione.stepSolvers.OlympusAgentSolver;
|
||||||
import com.olympus.hermione.stepSolvers.OlynmpusChatClientSolver;
|
import com.olympus.hermione.stepSolvers.OlynmpusChatClientSolver;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -265,12 +264,12 @@ public class ScenarioExecutionService {
|
|||||||
case "RAG_SOURCE_CODE":
|
case "RAG_SOURCE_CODE":
|
||||||
solver = new SourceCodeRagSolver();
|
solver = new SourceCodeRagSolver();
|
||||||
break;
|
break;
|
||||||
case "EXTERNAL_AGENT":
|
|
||||||
solver = new ExternalAgentSolver();
|
|
||||||
break;
|
|
||||||
case "EXTERNAL_CODEGENIE":
|
case "EXTERNAL_CODEGENIE":
|
||||||
solver = new ExternalCodeGenieSolver();
|
solver = new ExternalCodeGenieSolver();
|
||||||
break;
|
break;
|
||||||
|
case "OLYMPUS_AGENT":
|
||||||
|
solver = new OlympusAgentSolver();
|
||||||
|
break;
|
||||||
case "SUMMARIZE_DOC":
|
case "SUMMARIZE_DOC":
|
||||||
solver = new SummarizeDocSolver();
|
solver = new SummarizeDocSolver();
|
||||||
break;
|
break;
|
||||||
@@ -291,6 +290,9 @@ public class ScenarioExecutionService {
|
|||||||
logger.info("Solving step: " + step.getStepId());
|
logger.info("Solving step: " + step.getStepId());
|
||||||
scenarioExecution.setCurrentStepId(step.getStepId());
|
scenarioExecution.setCurrentStepId(step.getStepId());
|
||||||
scenarioExecution.setCurrentStepDescription(step.getName());
|
scenarioExecution.setCurrentStepDescription(step.getName());
|
||||||
|
|
||||||
|
// Save immediately so frontend can show correct current step
|
||||||
|
scenarioExecutionRepository.save(scenarioExecution);
|
||||||
|
|
||||||
ScenarioExecution scenarioExecutionNew = scenarioExecution;
|
ScenarioExecution scenarioExecutionNew = scenarioExecution;
|
||||||
|
|
||||||
|
|||||||
@@ -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<String> request = new HttpEntity<>(requestBody.toString(), headers);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ResponseEntity<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user