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
5 changes: 5 additions & 0 deletions Brainarr.Plugin/BrainarrSettings.Discovery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,11 @@ public bool EnableAutoDetection
HelpLink = "https://github.com/RicherTunes/Brainarr/wiki/Review-Queue")]
public int ReviewActionCooldownMinutes { get; set; } = 15;

[FieldDefinition(43, Label = "Enable Provider Calibration", Type = FieldType.Checkbox, Advanced = true, Hidden = HiddenType.Hidden,
HelpText = "Apply per-provider confidence calibration to triage scores. Disable for raw uncalibrated scores.",
HelpLink = "https://github.com/RicherTunes/Brainarr/wiki/Confidence-Calibration")]
public bool EnableProviderCalibration { get; set; } = true;

// Observability (hidden preview)
[FieldDefinition(16, Label = "Observability (Preview)", Type = FieldType.TagSelect, Advanced = true,
HelpText = "Compact preview of provider/model latency, errors and throttles.",
Expand Down
2 changes: 1 addition & 1 deletion Brainarr.Plugin/Services/Core/BrainarrOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ public object HandleAction(string action, IDictionary<string, string> query, Bra
"review/simulateapply" => _reviewQueueHandler.SimulateReviewApply(settings, query),
"review/applytriage" => _reviewQueueHandler.ApplyTriageSuggestions(settings, query),
"review/rollbacktriage" => _reviewQueueHandler.RollbackTriageApplication(query),
"review/explain" => _reviewQueueHandler.ExplainItem(settings, query, settings?.Provider),
"review/explain" => _reviewQueueHandler.ExplainItem(settings, query, settings?.EnableProviderCalibration == true ? settings?.Provider : null),
"review/clear" => _reviewQueueHandler.ClearApprovalSelections(settings),
"review/rejectselected" => _reviewQueueHandler.RejectOrNeverSelected(settings, query, ReviewQueueService.ReviewStatus.Rejected),
"review/neverselected" => _reviewQueueHandler.RejectOrNeverSelected(settings, query, ReviewQueueService.ReviewStatus.Never),
Expand Down
32 changes: 12 additions & 20 deletions Brainarr.Plugin/Services/Core/RecommendationTriageAdvisor.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Lidarr.Plugin.Common.Abstractions.Triage;
using NzbDrone.Core.ImportLists.Brainarr;
using NzbDrone.Core.ImportLists.Brainarr.Models;
using NzbDrone.Core.ImportLists.Brainarr.Services.Support;
using CommonBand = Lidarr.Plugin.Common.Abstractions.Triage.ConfidenceBand;

namespace NzbDrone.Core.ImportLists.Brainarr.Services.Core
{
internal sealed class RecommendationTriageAdvisor
{
internal static class ReasonCodes
{
public const string ConfidenceBelowThreshold = "CONFIDENCE_BELOW_THRESHOLD";
public const string ConfidenceFarBelowThreshold = "CONFIDENCE_FAR_BELOW_THRESHOLD";
public const string MissingRequiredMbids = "MISSING_REQUIRED_MBIDS";
public const string DuplicateSignal = "DUPLICATE_SIGNAL";
public const string HighConfidenceWithMbid = "HIGH_CONFIDENCE_WITH_MBID";
public const string ConsistentSignals = "CONSISTENT_SIGNALS";
public const string CalibrationApplied = "CALIBRATION_APPLIED";
public const string LowCalibrationProvider = "LOW_CALIBRATION_PROVIDER";
}

private static readonly string[] DuplicateSignals =
{
Expand Down Expand Up @@ -60,15 +51,15 @@ void AddReason(string code, string message, int weight)
{
var calibrated = profile.Calibrate(confidence);
AddReason(
ReasonCodes.CalibrationApplied,
TriageReasonCodes.CalibrationApplied,
$"provider {profile.ProviderName} calibration: {confidence:F2} -> {calibrated:F2} (scale={profile.Scale:F2}, bias={profile.Bias:F2})",
0);
confidence = calibrated;

if (profile.QualityTier < 0.6)
{
AddReason(
ReasonCodes.LowCalibrationProvider,
TriageReasonCodes.LowCalibrationProvider,
$"provider {profile.ProviderName} has low quality tier ({profile.QualityTier:F2})",
1);
}
Expand All @@ -78,14 +69,14 @@ void AddReason(string code, string message, int weight)
if (confidence < minConfidence)
{
AddReason(
ReasonCodes.ConfidenceBelowThreshold,
TriageReasonCodes.ConfidenceBelowThreshold,
$"confidence {confidence:F2} below threshold {minConfidence:F2}",
2);
}

if (confidence < (minConfidence - 0.15))
{
AddReason(ReasonCodes.ConfidenceFarBelowThreshold, "confidence substantially below threshold", 2);
AddReason(TriageReasonCodes.ConfidenceFarBelowThreshold, "confidence substantially below threshold", 2);
}

if (settings.RequireMbids)
Expand All @@ -96,29 +87,30 @@ void AddReason(string code, string message, int weight)

if (artistMissing || (needsAlbumMbid && albumMissing))
{
AddReason(ReasonCodes.MissingRequiredMbids, "missing required MusicBrainz identifiers", 2);
AddReason(TriageReasonCodes.MissingRequiredMbids, "missing required MusicBrainz identifiers", 2);
}
}

if (ContainsDuplicateSignal(item.Reason) || ContainsDuplicateSignal(item.Notes))
{
AddReason(ReasonCodes.DuplicateSignal, "duplicate-like signal in recommendation rationale", 3);
AddReason(TriageReasonCodes.DuplicateSignal, "duplicate-like signal in recommendation rationale", 3);
}

if (confidence >= 0.9 && !string.IsNullOrWhiteSpace(item.ArtistMusicBrainzId))
{
var reducedBy = Math.Min(1, riskScore);
if (reducedBy > 0)
{
AddReason(ReasonCodes.HighConfidenceWithMbid, "high confidence with artist MBID present", -reducedBy);
AddReason(TriageReasonCodes.HighConfidenceWithMbid, "high confidence with artist MBID present", -reducedBy);
}
}

var suggestedAction = riskScore >= 6 ? "reject" : riskScore >= 3 ? "review" : "accept";
var confidenceBand = confidence >= 0.8 ? "high" : confidence >= 0.6 ? "medium" : "low";
var band = confidence >= 0.8 ? CommonBand.High : confidence >= 0.6 ? CommonBand.Medium : CommonBand.Low;
var confidenceBand = band.ToString().ToLowerInvariant();
if (detailedReasons.Count == 0)
{
detailedReasons.Add(new ReviewTriageReason(ReasonCodes.ConsistentSignals, "signals look consistent for queue approval", 0));
detailedReasons.Add(new ReviewTriageReason(TriageReasonCodes.ConsistentSignals, "signals look consistent for queue approval", 0));
}

return new ReviewTriageResult(suggestedAction, confidenceBand, riskScore, detailedReasons, provider?.ToString());
Expand Down
4 changes: 2 additions & 2 deletions Brainarr.Plugin/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@
"lidarr.plugin.abstractions": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "[8.0.1, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.3, )",
"System.Text.Json": "[8.0.6, )"
}
},
Expand Down Expand Up @@ -480,4 +480,4 @@
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using FluentAssertions;
using Lidarr.Plugin.Common.Abstractions.Triage;
using NzbDrone.Core.ImportLists.Brainarr;
using NzbDrone.Core.ImportLists.Brainarr.Configuration;
using NzbDrone.Core.ImportLists.Brainarr.Services.Core;
using NzbDrone.Core.ImportLists.Brainarr.Services.Support;
using Xunit;
Expand Down Expand Up @@ -32,7 +34,7 @@ public void Analyze_ShouldSuggestReject_ForLowConfidenceDuplicateSignals()
result.SuggestedAction.Should().Be("reject");
result.RiskScore.Should().BeGreaterOrEqualTo(6);
result.Reasons.Should().Contain(x => x.Contains("duplicate"));
result.ReasonCodes.Should().Contain(RecommendationTriageAdvisor.ReasonCodes.DuplicateSignal);
result.ReasonCodes.Should().Contain(TriageReasonCodes.DuplicateSignal);
result.DetailedReasons.Should().Contain(x => x.Weight > 0);
}

Expand Down Expand Up @@ -60,7 +62,7 @@ public void Analyze_ShouldSuggestAccept_ForHighConfidenceWithMbids()

result.SuggestedAction.Should().Be("accept");
result.ConfidenceBand.Should().Be("high");
result.ReasonCodes.Should().Contain(RecommendationTriageAdvisor.ReasonCodes.ConsistentSignals);
result.ReasonCodes.Should().Contain(TriageReasonCodes.ConsistentSignals);
result.RiskScore.Should().Be(0);
}

Expand All @@ -85,8 +87,42 @@ public void Analyze_ShouldIncludeNegativeWeightReason_WhenHighConfidenceOffsetsR
var result = advisor.Analyze(item, settings);

result.SuggestedAction.Should().Be("accept");
result.ReasonCodes.Should().Contain(RecommendationTriageAdvisor.ReasonCodes.HighConfidenceWithMbid);
result.ReasonCodes.Should().Contain(TriageReasonCodes.HighConfidenceWithMbid);
result.DetailedReasons.Should().Contain(x => x.Weight < 0);
}

[Fact]
public void CalibrationDisabled_ReturnsRawScores()
{
var advisor = new RecommendationTriageAdvisor();
var settings = new BrainarrSettings
{
MinConfidence = 0.7,
RequireMbids = false
};

var item = new ReviewQueueService.ReviewItem
{
Artist = "A",
Album = "B",
Confidence = 0.75,
ArtistMusicBrainzId = "artist-mbid"
};

// With calibration disabled (provider=null), raw scores are used
var rawResult = advisor.Analyze(item, settings, provider: null);

// With calibration enabled (Ollama has Scale=0.80, Bias=0.05), scores differ
var calibratedResult = advisor.Analyze(item, settings, provider: AIProvider.Ollama);

// Calibrated result should have the CalibrationApplied reason code
calibratedResult.ReasonCodes.Should().Contain(TriageReasonCodes.CalibrationApplied);

// Raw result should NOT have the CalibrationApplied reason code
rawResult.ReasonCodes.Should().NotContain(TriageReasonCodes.CalibrationApplied);

// The calibrated result should also flag the low-quality provider
calibratedResult.ReasonCodes.Should().Contain(TriageReasonCodes.LowCalibrationProvider);
}
}
}
4 changes: 2 additions & 2 deletions Brainarr.Tests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@
"lidarr.plugin.abstractions": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "[8.0.1, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.3, )",
"System.Text.Json": "[8.0.6, )"
}
},
Expand Down Expand Up @@ -665,4 +665,4 @@
}
}
}
}
}
4 changes: 2 additions & 2 deletions tests/Brainarr.Providers.OpenAI.Tests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@
"lidarr.plugin.abstractions": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "[8.0.1, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.3, )",
"System.Text.Json": "[8.0.6, )"
}
},
Expand Down Expand Up @@ -585,4 +585,4 @@
}
}
}
}
}
4 changes: 2 additions & 2 deletions tests/Brainarr.TestKit.Providers/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@
"lidarr.plugin.abstractions": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "[8.0.1, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.3, )",
"System.Text.Json": "[8.0.6, )"
}
},
Expand Down Expand Up @@ -490,4 +490,4 @@
}
}
}
}
}
Loading