Skip to content
4 changes: 2 additions & 2 deletions src/Bellatrix.Desktop/llm/FindByPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,11 @@ private string TryResolveFromPageObjectMemory(string instruction, WindowsDriver<
private string ResolveViaPromptFallback(string location, WindowsDriver<WindowsElement> driver, int maxAttempts = 3)
{
var viewSnapshotProvider = ServicesCollection.Current.Resolve<IViewSnapshotProvider>();
var summaryJson = viewSnapshotProvider.GetCurrentViewSnapshot();
var failedSelectors = new List<string>();

for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
var summaryJson = viewSnapshotProvider.GetCurrentViewSnapshot();
var prompt = SemanticKernelService.Kernel?.InvokeAsync(nameof(LocatorSkill), nameof(LocatorSkill.BuildLocatorPrompt),
new()
{
Expand Down Expand Up @@ -229,4 +229,4 @@ private static bool IsElementPresent(WindowsDriver<WindowsElement> driver, strin
}

public override string ToString() => $"Prompt = {Value}";
}
}
4 changes: 2 additions & 2 deletions src/Bellatrix.Mobile/llm/android/FindByPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,11 @@ private string TryResolveFromPageObjectMemory(string instruction, object context
private string ResolveViaPromptFallback(string location, object context, int maxAttempts = 3)
{
var snapshotProvider = ServicesCollection.Current.Resolve<IViewSnapshotProvider>();
var summaryJson = snapshotProvider.GetCurrentViewSnapshot();
var failedSelectors = new List<string>();

for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
var summaryJson = snapshotProvider.GetCurrentViewSnapshot();
var prompt = SemanticKernelService.Kernel.InvokeAsync(nameof(AndroidLocatorSkill), nameof(AndroidLocatorSkill.BuildLocatorPrompt), new()
{
["viewSummaryJson"] = summaryJson,
Expand Down Expand Up @@ -224,4 +224,4 @@ private static bool IsElementPresent(object context, string uiautomator)
}

public override string ToString() => $"Prompt = {Value}";
}
}
4 changes: 2 additions & 2 deletions src/Bellatrix.Mobile/llm/ios/FindByPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,11 @@ private string TryResolveFromPageObjectMemory(string instruction, object context
private string ResolveViaPromptFallback(string location, object context, int maxAttempts = 3)
{
var snapshotProvider = ServicesCollection.Current.Resolve<IViewSnapshotProvider>();
var summaryJson = snapshotProvider.GetCurrentViewSnapshot();
var failedSelectors = new List<string>();

for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
var summaryJson = snapshotProvider.GetCurrentViewSnapshot();
var prompt = SemanticKernelService.Kernel?.InvokeAsync(nameof(IOSLocatorSkill), nameof(IOSLocatorSkill.BuildLocatorPrompt),
new()
{
Expand Down Expand Up @@ -227,4 +227,4 @@ private static bool IsElementPresent(object context, string nspredicate)
}

public override string ToString() => $"Prompt = {Value}";
}
}
55 changes: 53 additions & 2 deletions src/Bellatrix.Playwright/components/common/CheckBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,32 @@ public class CheckBox : Component, IComponentDisabled, IComponentChecked, ICompo
/// <param name="options"></param>
public virtual void Check(LocatorCheckOptions options = default)
{
DefaultCheck(Checking, Checked, options);
var tempOptions = options ?? new LocatorCheckOptions();
tempOptions.Timeout = 1;

Checking?.Invoke(this, new ComponentActionEventArgs(this));
this.ValidateIsPresent();

try
{
DefaultCheck(null, null, tempOptions);
}
catch
{
// Fallback to JsClick, checkbox may be custom element with hidden input

if (!IsChecked)
{
var clickOptions = new LocatorClickOptions
{
Force = true,
Timeout = options?.Timeout,
};
DefaultClick(null, null, clickOptions);
}
}

Checked?.Invoke(this, new ComponentActionEventArgs(this));
}

/// <summary>
Expand All @@ -44,7 +69,33 @@ public virtual void Check(LocatorCheckOptions options = default)
/// <param name="options"></param>
public virtual void Uncheck(LocatorUncheckOptions options = default)
{
DefaultUncheck(Unchecking, Unchecked, options);
var tempOptions = options ?? new LocatorUncheckOptions();
tempOptions.Timeout = 1;

Unchecking?.Invoke(this, new ComponentActionEventArgs(this));
this.ValidateIsPresent();

try
{
DefaultUncheck(null, null, tempOptions);
}
catch
{
// Fallback to JsClick, checkbox may be custom element with hidden input

if (IsChecked)
{
var clickOptions = new LocatorClickOptions
{
Force = true,
Timeout = options?.Timeout,
};
DefaultClick(null, null, clickOptions);
}

}

Unchecked?.Invoke(this, new ComponentActionEventArgs(this));
}

public virtual void Hover()
Expand Down
24 changes: 23 additions & 1 deletion src/Bellatrix.Playwright/components/common/RadioButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,28 @@ public virtual void Hover()
/// <param name="options"></param>
public virtual void Click(LocatorCheckOptions options = default)
{
DefaultCheck(Clicking, Clicked, options);
var tempOptions = options ?? new LocatorCheckOptions();
tempOptions.Timeout = 1;

Clicking?.Invoke(this, new ComponentActionEventArgs(this));
this.ValidateIsPresent();

try
{
DefaultCheck(null, null, tempOptions);
}
catch
{
// Fallback to JsClick, radio button may be custom element with hidden input
var clickOptions = new LocatorClickOptions
{
Force = true,
Timeout = options?.Timeout,
};

DefaultClick(null, null, clickOptions);
}

Clicked?.Invoke(this, new ComponentActionEventArgs(this));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ internal void DefaultClick(EventHandler<ComponentActionEventArgs> clicking, Even
if (options.Force != null && (bool)options.Force) PerformJsClick();
else WrappedElement.Click(options);
}

else WrappedElement.Click();

clicked?.Invoke(this, new ComponentActionEventArgs(this));
Expand All @@ -73,7 +73,6 @@ internal void DefaultUncheck(EventHandler<ComponentActionEventArgs> unchecking,

WrappedElement.Uncheck(options);


@unchecked?.Invoke(this, new ComponentActionEventArgs(this));
}

Expand Down
2 changes: 1 addition & 1 deletion src/Bellatrix.Playwright/components/core/Component.cs
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ public bool IsPresent
{
try
{
return WrappedElement.ElementHandle(new LocatorElementHandleOptions { Timeout = ConfigurationService.GetSection<WebSettings>().TimeoutSettings.InMilliseconds().ElementToExistTimeout }) != null;
return WrappedElement.IsPresent;
}
catch
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ public override void UnsubscribeToAll()
RadioButton.Hovering -= HoveringEventHandler;
RadioButton.Hovered -= HoveredEventHandler;
}
}
}
111 changes: 67 additions & 44 deletions src/Bellatrix.Playwright/llm/FindByPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,15 @@
using Bellatrix.LLM;
using Bellatrix.LLM.Plugins;
using Bellatrix.Playwright.LLM.Plugins;
using Bellatrix.Playwright.Locators;
using Microsoft.Identity.Client;
using Microsoft.SemanticKernel;
using Pipelines.Sockets.Unofficial.Arenas;
using System.Text.RegularExpressions;
using System.Threading;

namespace Bellatrix.Playwright.LLM;

public class FindByPrompt : FindStrategy
{
private bool _tryResolveFromPages = true;
private readonly bool _tryResolveFromPages;
/// <summary>
/// Initializes a new instance of the <see cref="FindByPrompt"/> class with the specified prompt value.
/// </summary>
Expand Down Expand Up @@ -58,51 +55,80 @@ private FindStrategy TryResolveLocator(string location, IViewSnapshotProvider sn
{
if (_tryResolveFromPages)
{
// Try from memory
var match = SemanticKernelService.Memory
.SearchAsync(Value, index: "PageObjects", limit: 1)
.Result.Results.FirstOrDefault();

if (match != null)
var ragLocator = TryResolveFromPageObjectMemory(Value);
if (ragLocator != null && ragLocator.Resolve(WrappedBrowser.CurrentPage).IsPresent)
{
var pageSummary = match.Partitions.FirstOrDefault()?.Text ?? "";
var mappedPrompt = SemanticKernelService.Kernel
.InvokeAsync(nameof(LocatorMapperSkill), nameof(LocatorMapperSkill.MatchPromptToKnownLocator), new()
{
["pageSummary"] = pageSummary,
["instruction"] = Value
}).Result.GetValue<string>();

var result = SemanticKernelService.Kernel.InvokePromptAsync(mappedPrompt).Result;
var rawLocator = result?.GetValue<string>()?.Trim();
var ragLocator = new FindXpathStrategy(rawLocator);
if (ragLocator != null)
{
Logger.LogInformation($"✅ Using RAG-located element '{ragLocator}' For '${Value}'");
return ragLocator;
}
Logger.LogInformation($"✅ Using RAG-located element '{ragLocator}' For '${Value}'");
return ragLocator;
}
}

// Try cache
var cached = LocatorCacheService.TryGetCached(location, Value);
if (!string.IsNullOrEmpty(cached))
// Step 2: Try local persistent cache
Logger.LogInformation("⚠️ RAG-located element not present. Trying cached selectors...");
var cached = LocatorCacheService.TryGetCached(WrappedBrowser.CurrentPage.Url, Value);

var strategy = new FindXpathStrategy(cached);
if (!string.IsNullOrEmpty(cached) && strategy.Resolve(WrappedBrowser.CurrentPage).IsPresent)
{
return new FindXpathStrategy(cached);
Logger.LogInformation("✅ Using cached selector.");
return strategy;
}

// Remove broken and fall back
LocatorCacheService.Remove(location, Value);
// Step 3: Fall back to AI + prompt regeneration
Logger.LogInformation("⚠️ Cached selector failed or not found. Re-querying AI...");
LocatorCacheService.Remove(WrappedBrowser.CurrentPage.Url, Value);

return ResolveViaPromptFallback(location, snapshotProvider);
}

private static FindXpathStrategy TryResolveFromPageObjectMemory(string instruction)
{
var match = SemanticKernelService.Memory
.SearchAsync(instruction, index: "PageObjects", limit: 1)
.Result.Results.FirstOrDefault();

if (match == null) return null;

var pageSummary = match.Partitions.FirstOrDefault()?.Text ?? string.Empty;
var mappedPrompt = SemanticKernelService.Kernel
.InvokeAsync(nameof(LocatorMapperSkill), nameof(LocatorMapperSkill.MatchPromptToKnownLocator),
new()
{
["pageSummary"] = pageSummary,
["instruction"] = instruction
}).Result.GetValue<string>();

var locatorResult = SemanticKernelService.Kernel
.InvokePromptAsync(mappedPrompt).Result.GetValue<string>();

return ParsePromptLocatorToStrategy(locatorResult);
}

private static FindXpathStrategy ParsePromptLocatorToStrategy(string promptResult)
{
if (promptResult == "Unknown")
{
return null;
}

var parts = Regex.Match(promptResult, @"^\s*xpath\s*=\s*(//.+)$", RegexOptions.IgnoreCase);
if (!parts.Success)
{
throw new ArgumentException($"❌ Invalid format. Expected: xpath=//... but received '{promptResult}'");
}

var xpath = parts.Groups[1].Value.Trim();

return new FindXpathStrategy(xpath);
}

private FindStrategy ResolveViaPromptFallback(string location, IViewSnapshotProvider snapshotProvider, int maxAttempts = 3)
{
var summaryJson = snapshotProvider.GetCurrentViewSnapshot();
var failedSelectors = new List<string>();

for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
var summaryJson = snapshotProvider.GetCurrentViewSnapshot();
var prompt = SemanticKernelService.Kernel
.InvokeAsync(nameof(LocatorSkill), nameof(LocatorSkill.BuildLocatorPrompt),
new()
Expand All @@ -113,25 +139,22 @@ private FindStrategy ResolveViaPromptFallback(string location, IViewSnapshotProv
}).Result.GetValue<string>();

var result = SemanticKernelService.Kernel.InvokePromptAsync(prompt).Result;
var raw = result?.GetValue<string>()?.Trim();
var rawSelector = result?.GetValue<string>()?.Trim();

if (!string.IsNullOrWhiteSpace(raw))
var strategy = new FindXpathStrategy(rawSelector);
if (!string.IsNullOrWhiteSpace(rawSelector) && strategy.Resolve(WrappedBrowser.CurrentPage).IsPresent)
{
var locator = new FindXpathStrategy(raw);
if (locator != null)
{
LocatorCacheService.Update(location, Value, locator.Value);
return locator;
}

failedSelectors.Add(raw);
LocatorCacheService.Update(location, Value, strategy.Value);
return strategy;
}

failedSelectors.Add(rawSelector);
Logger.LogInformation($"[Attempt {attempt}] Selector failed: {rawSelector}");
Thread.Sleep(300);
}

throw new ArgumentException($"❌ No valid locator found for: {Value}");
}

public override string ToString() => $"Prompt = {Value}";
}
}
11 changes: 9 additions & 2 deletions src/Bellatrix.Playwright/plugins/screenshots/ScreenshotEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ internal static class ScreenshotEngine
public static string TakeScreenshot(ServicesCollection serviceContainer, bool fullPage)
{
var browser = serviceContainer.Resolve<WrappedBrowser>();
return Convert.ToBase64String(browser.CurrentPage.Screenshot(new PageScreenshotOptions { FullPage = fullPage, Type = ScreenshotType.Png }));
if (browser is not null)
{
return Convert.ToBase64String(browser.CurrentPage.Screenshot(new PageScreenshotOptions { FullPage = fullPage, Type = ScreenshotType.Png }));
}
else
{
return string.Empty;
}
}

public static string GetEmbeddedResource(string resourceName, Assembly assembly)
Expand All @@ -44,4 +51,4 @@ private static string FormatResourceName(Assembly assembly, string resourceName)
.Replace("\\", ".")
.Replace("/", ".");
}
}
}
Loading
Loading