Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,13 @@
<version>${aws-sdk-batch.version}</version>
</dependency>

<!-- AWS Bedrock Agent Runtime (v2 SDK) -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bedrockagentruntime</artifactId>
<version>${aws-sdk-batch.version}</version>
</dependency>

<!-- ====================================== -->
<!-- JSON Processing -->
<!-- ====================================== -->
Expand Down
17 changes: 12 additions & 5 deletions src/main/java/activesupport/aws/s3/SecretsManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,19 @@ private static AWSSecretsManager awsClientSetup() {
}

public static String getSecretValue(String secretKey) {
if (cache.containsKey(secretKey)) {
return cache.get(secretKey);
return getSecretValue(secretsId, secretKey);
}

public static String getSecretValue(String secretName, String secretKey) {
String cacheKey = secretName + ":" + secretKey;
if (cache.containsKey(cacheKey)) {
return cache.get(cacheKey);
}

String secret = null;

GetSecretValueRequest getSecretValueRequest = new GetSecretValueRequest()
.withSecretId(secretsId);
.withSecretId(secretName);
GetSecretValueResult getSecretValueResult = null;

try {
Expand All @@ -92,8 +97,10 @@ public static String getSecretValue(String secretKey) {
if (getSecretValueResult != null && getSecretValueResult.getSecretString() != null) {
secret = getSecretValueResult.getSecretString();
JsonObject jsonObject = JsonParser.parseString(secret).getAsJsonObject();
secret = jsonObject.get(secretKey).getAsString();
cache.put(secretKey, secret);
if (jsonObject.has(secretKey)) {
secret = jsonObject.get(secretKey).getAsString();
cache.put(cacheKey, secret);
}
}
return secret;
}
Expand Down
140 changes: 140 additions & 0 deletions src/main/java/activesupport/selfhealing/BedrockSelectorService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package activesupport.selfhealing;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.bedrockagentruntime.BedrockAgentRuntimeAsyncClient;
import software.amazon.awssdk.services.bedrockagentruntime.model.InvokeAgentRequest;
import software.amazon.awssdk.services.bedrockagentruntime.model.InvokeAgentResponseHandler;

import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class BedrockSelectorService {

private static final Logger LOGGER = LogManager.getLogger(BedrockSelectorService.class);
private static final ObjectMapper MAPPER = new ObjectMapper();

private BedrockAgentRuntimeAsyncClient client;

private BedrockAgentRuntimeAsyncClient getClient() {
if (client == null) {
client = BedrockAgentRuntimeAsyncClient.builder()
.region(Region.of(SelfHealingConfig.getRegion()))
.build();
}
return client;
}

public Optional<HealResult> findCorrectedSelector(String brokenSelector, String selectorType, String pageSource) {
try {
String truncatedDom = truncateDom(pageSource, SelfHealingConfig.getMaxDomLength());
String prompt = buildPrompt(brokenSelector, selectorType, truncatedDom);

LOGGER.info("SELF-HEALING: Querying Bedrock agent for broken {} selector: {}", selectorType, brokenSelector);

String sessionId = UUID.randomUUID().toString();
StringBuilder responseText = new StringBuilder();

InvokeAgentResponseHandler handler = InvokeAgentResponseHandler.builder()
.onResponse(response -> LOGGER.debug("SELF-HEALING: Agent response received"))
.subscriber(InvokeAgentResponseHandler.Visitor.builder()
.onChunk(chunk -> {
if (chunk.bytes() != null) {
responseText.append(chunk.bytes().asUtf8String());
}
})
.build())
.onError(error -> LOGGER.error("SELF-HEALING: Agent streaming error", error))
.build();

CompletableFuture<Void> future = getClient().invokeAgent(
InvokeAgentRequest.builder()
.agentId(SelfHealingConfig.getAgentId())
.agentAliasId(SelfHealingConfig.getAgentAliasId())
.sessionId(sessionId)
.inputText(prompt)
.build(),
handler
);

future.get(30, TimeUnit.SECONDS);

return parseResponse(responseText.toString());

} catch (Exception e) {
LOGGER.warn("SELF-HEALING: Failed to query Bedrock agent: {}", e.getMessage());
return Optional.empty();
}
}

private String buildPrompt(String brokenSelector, String selectorType, String dom) {
return String.format(
"You are a Selenium selector repair assistant. A %s selector is broken and needs fixing.\n\n"
+ "BROKEN SELECTOR: %s\n\n"
+ "RULES:\n"
+ "1. Analyse the HTML below and find the correct selector for the same target element.\n"
+ "2. PRESERVE positional indices like [2], [last()], [position()>1] etc. — they exist because "
+ "multiple elements match and the test targets a specific one (e.g. a visible checkbox vs a hidden input).\n"
+ "3. The corrected selector MUST target a VISIBLE, INTERACTABLE element — never a type=\"hidden\" input "
+ "when the original clearly intended a clickable/visible control.\n"
+ "4. Only fix what is broken (e.g. a typo in an attribute value). Do not restructure or simplify the selector.\n"
+ "5. Respond ONLY with JSON: {\"selector\": \"...\", \"confidence\": 0.0-1.0, \"reason\": \"...\"}\n\n"
+ "PAGE HTML:\n%s",
selectorType, brokenSelector, dom
);
}

private Optional<HealResult> parseResponse(String response) {
try {
String jsonStr = extractJson(response);
JsonNode json = MAPPER.readTree(jsonStr);

String selector = json.get("selector").asText();
double confidence = json.get("confidence").asDouble();
String reason = json.has("reason") ? json.get("reason").asText() : "unknown";

if (selector == null || selector.isBlank()) {
LOGGER.warn("SELF-HEALING: Agent returned empty selector");
return Optional.empty();
}

LOGGER.info("SELF-HEALING: Agent suggested selector: {} (confidence: {}, reason: {})",
selector, confidence, reason);

return Optional.of(new HealResult(selector, confidence, reason));

} catch (Exception e) {
LOGGER.warn("SELF-HEALING: Failed to parse agent response: {} | Raw response: {}",
e.getMessage(), response);
return Optional.empty();
}
}

private String extractJson(String response) {
int start = response.indexOf('{');
int end = response.lastIndexOf('}');
if (start >= 0 && end > start) {
return response.substring(start, end + 1);
}
return response;
}

static String truncateDom(String pageSource, int maxLength) {
if (pageSource == null) return "";
// Strip script and style content to maximise useful DOM in the context window
String cleaned = pageSource
.replaceAll("(?s)<script[^>]*>.*?</script>", "")
.replaceAll("(?s)<style[^>]*>.*?</style>", "")
.replaceAll("\\s{2,}", " ");

if (cleaned.length() <= maxLength) {
return cleaned;
}
return cleaned.substring(0, maxLength) + "\n<!-- DOM TRUNCATED -->";
}
}
31 changes: 31 additions & 0 deletions src/main/java/activesupport/selfhealing/HealResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package activesupport.selfhealing;

public class HealResult {

private final String selector;
private final double confidence;
private final String reason;

public HealResult(String selector, double confidence, String reason) {
this.selector = selector;
this.confidence = confidence;
this.reason = reason;
}

public String getSelector() {
return selector;
}

public double getConfidence() {
return confidence;
}

public String getReason() {
return reason;
}

@Override
public String toString() {
return String.format("HealResult{selector='%s', confidence=%.2f, reason='%s'}", selector, confidence, reason);
}
}
56 changes: 56 additions & 0 deletions src/main/java/activesupport/selfhealing/SelfHealingConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package activesupport.selfhealing;

import activesupport.aws.s3.SecretsManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public final class SelfHealingConfig {

private static final Logger LOGGER = LogManager.getLogger(SelfHealingConfig.class);
private static final String PREFIX = "selfHealing.";
private static final String DEFAULT_SECRET_NAME = "vol-functional-tests/bedrock";

private SelfHealingConfig() {
}

public static boolean isEnabled() {
return Boolean.parseBoolean(System.getProperty(PREFIX + "enabled", "false"));
}

public static String getRegion() {
return System.getProperty(PREFIX + "region", "eu-west-1");
}

public static String getAgentId() {
return getConfigValue("selfHeal_agentId");
}

public static String getAgentAliasId() {
return getConfigValue("selfHealth_agentAliasId");
}

public static int getMaxDomLength() {
return Integer.parseInt(System.getProperty(PREFIX + "maxDomLength", "50000"));
}

public static double getMinConfidence() {
return Double.parseDouble(System.getProperty(PREFIX + "minConfidence", "0.5"));
}

/**
* Resolves a config value: system property first, then AWS Secrets Manager.
*/
private static String getConfigValue(String key) {
String prop = System.getProperty(PREFIX + key);
if (prop != null && !prop.isBlank()) {
return prop;
}
try {
String secretName = System.getProperty(PREFIX + "secretName", DEFAULT_SECRET_NAME);
return SecretsManager.getSecretValue(secretName, key);
} catch (Exception e) {
LOGGER.warn("SELF-HEALING: Failed to fetch '{}' from Secrets Manager: {}", key, e.getMessage());
return "";
}
}
}
Loading
Loading