Skip to content

Commit e074746

Browse files
shanselmanCopilot
andcommitted
fix: honour PreferStructuredCategories in notification pipeline (closes #104)
Adds preferStructuredCategories parameter to Classify() — when false, structured metadata (Intent, Channel) is skipped, going straight to user rules and keyword fallback. Reimplemented on current master (post-#93 UserRules merge). 5 new tests covering all code paths. Co-authored-by: Copilot <[email protected]>
1 parent 37a5f94 commit e074746

File tree

4 files changed

+68
-8
lines changed

4 files changed

+68
-8
lines changed

src/OpenClaw.Shared/NotificationCategorizer.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,22 @@ public class NotificationCategorizer
5151

5252
/// <summary>
5353
/// Classify a notification using the layered pipeline.
54+
/// When <paramref name="preferStructuredCategories"/> is true (default),
55+
/// structured metadata (Intent, Channel) is checked first.
56+
/// When false, classification starts from user-defined rules then keyword fallback.
5457
/// </summary>
55-
public (string title, string type) Classify(OpenClawNotification notification, IReadOnlyList<UserNotificationRule>? userRules = null)
58+
public (string title, string type) Classify(OpenClawNotification notification, IReadOnlyList<UserNotificationRule>? userRules = null, bool preferStructuredCategories = true)
5659
{
57-
// 1. Structured metadata: Intent
58-
if (!string.IsNullOrEmpty(notification.Intent) && IntentMap.TryGetValue(notification.Intent, out var intentResult))
59-
return intentResult;
60+
if (preferStructuredCategories)
61+
{
62+
// 1. Structured metadata: Intent
63+
if (!string.IsNullOrEmpty(notification.Intent) && IntentMap.TryGetValue(notification.Intent, out var intentResult))
64+
return intentResult;
6065

61-
// 2. Structured metadata: Channel
62-
if (!string.IsNullOrEmpty(notification.Channel) && ChannelMap.TryGetValue(notification.Channel, out var channelResult))
63-
return channelResult;
66+
// 2. Structured metadata: Channel
67+
if (!string.IsNullOrEmpty(notification.Channel) && ChannelMap.TryGetValue(notification.Channel, out var channelResult))
68+
return channelResult;
69+
}
6470

6571
// 3. User-defined rules (pattern match on title + message)
6672
if (userRules is { Count: > 0 })

src/OpenClaw.Shared/OpenClawGatewayClient.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ public class OpenClawGatewayClient : WebSocketClientBase
2424
private bool _sessionPreviewUnsupported;
2525
private bool _nodeListUnsupported;
2626
private IReadOnlyList<UserNotificationRule>? _userRules;
27+
private bool _preferStructuredCategories = true;
28+
29+
/// <summary>
30+
/// Controls whether structured notification metadata (Intent, Channel) takes priority
31+
/// over keyword-based classification. Call after construction and whenever settings change.
32+
/// </summary>
33+
public void SetPreferStructuredCategories(bool value) => _preferStructuredCategories = value;
2734

2835
private void ResetUnsupportedMethodFlags()
2936
{
@@ -1570,7 +1577,7 @@ private void EmitNotification(string text)
15701577
{
15711578
Message = text.Length > 200 ? text[..200] + "…" : text
15721579
};
1573-
var (title, type) = _categorizer.Classify(notification, _userRules);
1580+
var (title, type) = _categorizer.Classify(notification, _userRules, _preferStructuredCategories);
15741581
notification.Title = title;
15751582
notification.Type = type;
15761583
NotificationReceived?.Invoke(this, notification);

src/OpenClaw.Tray.WinUI/App.xaml.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,6 +1097,7 @@ private void InitializeGatewayClient()
10971097

10981098
_gatewayClient = new OpenClawGatewayClient(_settings.GatewayUrl, _settings.Token, new AppLogger());
10991099
_gatewayClient.SetUserRules(_settings.UserRules.Count > 0 ? _settings.UserRules : null);
1100+
_gatewayClient.SetPreferStructuredCategories(_settings.PreferStructuredCategories);
11001101
_gatewayClient.StatusChanged += OnConnectionStatusChanged;
11011102
_gatewayClient.ActivityChanged += OnActivityChanged;
11021103
_gatewayClient.NotificationReceived += OnNotificationReceived;

tests/OpenClaw.Shared.Tests/NotificationCategorizerTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,52 @@ public void PipelineOrder_Intent_Channel_UserRules_Keywords()
272272
Assert.Equal("health", _categorizer.Classify(notification).type);
273273
}
274274

275+
// --- PreferStructuredCategories = false ---
276+
277+
[Fact]
278+
public void PreferStructuredCategories_False_SkipsIntent()
279+
{
280+
var notification = new OpenClawNotification { Message = "New email notification", Intent = "build" };
281+
var (_, type) = _categorizer.Classify(notification, preferStructuredCategories: false);
282+
Assert.Equal("email", type);
283+
}
284+
285+
[Fact]
286+
public void PreferStructuredCategories_False_SkipsChannel()
287+
{
288+
var notification = new OpenClawNotification { Message = "Check your email", Channel = "calendar" };
289+
var (_, type) = _categorizer.Classify(notification, preferStructuredCategories: false);
290+
Assert.Equal("email", type);
291+
}
292+
293+
[Fact]
294+
public void PreferStructuredCategories_False_UserRulesStillApply()
295+
{
296+
var rules = new List<UserNotificationRule>
297+
{
298+
new() { Pattern = "invoice", Category = "email", Enabled = true }
299+
};
300+
var notification = new OpenClawNotification { Message = "New invoice received", Intent = "urgent" };
301+
var (_, type) = _categorizer.Classify(notification, rules, preferStructuredCategories: false);
302+
Assert.Equal("email", type);
303+
}
304+
305+
[Fact]
306+
public void PreferStructuredCategories_False_FallsBackToKeywords()
307+
{
308+
var notification = new OpenClawNotification { Message = "Hello world", Intent = "build", Channel = "email" };
309+
var (_, type) = _categorizer.Classify(notification, preferStructuredCategories: false);
310+
Assert.Equal("info", type);
311+
}
312+
313+
[Fact]
314+
public void PreferStructuredCategories_True_Default_BehaviourUnchanged()
315+
{
316+
var notification = new OpenClawNotification { Message = "New email notification", Intent = "build" };
317+
Assert.Equal("build", _categorizer.Classify(notification).type);
318+
Assert.Equal("build", _categorizer.Classify(notification, preferStructuredCategories: true).type);
319+
}
320+
275321
// --- ClassifyByKeywords static method ---
276322

277323
[Fact]

0 commit comments

Comments
 (0)