Skip to content

Commit 9be1fa2

Browse files
committed
add skills
1 parent e238e24 commit 9be1fa2

6 files changed

Lines changed: 317 additions & 1 deletion

File tree

App.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
<add key="ProactiveAssistEnabled" value="true" />
4343
<add key="BehaviorLearningEnabled" value="true" />
4444
<add key="StuckNudgesEnabled" value="true" />
45+
<add key="LlmSkillAssistEnabled" value="true" />
4546
<!-- 安静时段(24小时制,跨天区间) -->
4647
<add key="QuietHoursStart" value="22" />
4748
<add key="QuietHoursEnd" value="8" />

LlmService.cs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,24 @@ public class LlmLearningMilestone
6464
public bool IsCompleted { get; set; }
6565
}
6666

67+
public class LlmSkillRecommendation
68+
{
69+
[JsonPropertyName("skill_id")]
70+
public string SkillId { get; set; }
71+
72+
[JsonPropertyName("title")]
73+
public string Title { get; set; }
74+
75+
[JsonPropertyName("why")]
76+
public string Why { get; set; }
77+
78+
[JsonPropertyName("next_step")]
79+
public string NextStep { get; set; }
80+
81+
[JsonPropertyName("confidence")]
82+
public double Confidence { get; set; }
83+
}
84+
6785
public class LlmService : ILlmService
6886
{
6987
private IOpenAIService _openAiService;
@@ -136,6 +154,18 @@ public class LlmService : ILlmService
136154
"\n\nUser behavior context (local profile, use as soft guidance only):\n{userContext}\n" +
137155
"Adapt tone and suggestions to reduce interruption. Keep one clear next step.";
138156

157+
private const string SkillRecommendationSystemPrompt =
158+
"You are a task execution copilot. Based on the task and context, recommend 1-3 skills that best help the user move forward now. " +
159+
"Allowed skill_id values ONLY: decompose, focus_sprint, priority_rebalance, risk_check, delegate_prepare, clarify_goal. " +
160+
"Return ONLY a valid JSON array. Each item must contain: " +
161+
"\"skill_id\", \"title\", \"why\", \"next_step\", \"confidence\" (0 to 1). " +
162+
"Keep title/why/next_step concise and actionable.\n" +
163+
"Task: {taskDescription}\n" +
164+
"Importance: {importance}\n" +
165+
"Urgency: {urgency}\n" +
166+
"InactiveDuration: {inactiveDuration}\n" +
167+
"UserContext: {userContext}";
168+
139169
private const string ConversationTaskExtractPrompt =
140170
"You are an assistant helping a user capture personal action items from a multi-speaker conversation. " +
141171
"Extract ONLY tasks that the user should do. " +
@@ -533,6 +563,157 @@ internal static (string reminder, List<string> suggestions) ParseReminderRespons
533563
return ParseReminderResponse(llmResponse);
534564
}
535565

566+
public async Task<List<LlmSkillRecommendation>> RecommendTaskSkillsAsync(
567+
string taskDescription,
568+
string importance,
569+
string urgency,
570+
TimeSpan inactiveDuration,
571+
string userContext,
572+
int maxSkills = 3)
573+
{
574+
if (string.IsNullOrWhiteSpace(taskDescription))
575+
{
576+
return new List<LlmSkillRecommendation>();
577+
}
578+
579+
string context = string.IsNullOrWhiteSpace(userContext) ? "N/A" : userContext;
580+
string prompt = SkillRecommendationSystemPrompt
581+
.Replace("{taskDescription}", taskDescription)
582+
.Replace("{importance}", string.IsNullOrWhiteSpace(importance) ? "Unknown" : importance)
583+
.Replace("{urgency}", string.IsNullOrWhiteSpace(urgency) ? "Unknown" : urgency)
584+
.Replace("{inactiveDuration}", FormatTimeSpan(inactiveDuration))
585+
.Replace("{userContext}", context);
586+
587+
try
588+
{
589+
string llmResponse = await GetCompletionAsync(prompt);
590+
if (IsErrorResponse(llmResponse))
591+
{
592+
Console.WriteLine($"LLM skill recommendation failed for '{taskDescription}'. Fallback to rules. Response: {llmResponse}");
593+
return GetRuleBasedSkillRecommendations(taskDescription, importance, urgency, inactiveDuration, maxSkills);
594+
}
595+
596+
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
597+
var parsed = JsonSerializer.Deserialize<List<LlmSkillRecommendation>>(llmResponse, options) ?? new List<LlmSkillRecommendation>();
598+
var filtered = parsed
599+
.Where(s => s != null && IsAllowedSkillId(s.SkillId))
600+
.Select(s => NormalizeSkillRecommendation(s))
601+
.Take(Math.Max(1, maxSkills))
602+
.ToList();
603+
604+
if (filtered.Count > 0)
605+
{
606+
return filtered;
607+
}
608+
}
609+
catch (Exception ex)
610+
{
611+
Console.WriteLine($"Error parsing LLM skill recommendation, fallback to rules. {ex.Message}");
612+
}
613+
614+
return GetRuleBasedSkillRecommendations(taskDescription, importance, urgency, inactiveDuration, maxSkills);
615+
}
616+
617+
private static bool IsAllowedSkillId(string skillId)
618+
{
619+
if (string.IsNullOrWhiteSpace(skillId)) return false;
620+
string id = skillId.Trim().ToLowerInvariant();
621+
return id == "decompose"
622+
|| id == "focus_sprint"
623+
|| id == "priority_rebalance"
624+
|| id == "risk_check"
625+
|| id == "delegate_prepare"
626+
|| id == "clarify_goal";
627+
}
628+
629+
private static LlmSkillRecommendation NormalizeSkillRecommendation(LlmSkillRecommendation s)
630+
{
631+
return new LlmSkillRecommendation
632+
{
633+
SkillId = (s.SkillId ?? "clarify_goal").Trim().ToLowerInvariant(),
634+
Title = string.IsNullOrWhiteSpace(s.Title) ? "执行技能建议" : s.Title.Trim(),
635+
Why = string.IsNullOrWhiteSpace(s.Why) ? "帮助你更快推进任务。" : s.Why.Trim(),
636+
NextStep = string.IsNullOrWhiteSpace(s.NextStep) ? "先执行一个可在10分钟内完成的动作。" : s.NextStep.Trim(),
637+
Confidence = Math.Max(0, Math.Min(1, s.Confidence))
638+
};
639+
}
640+
641+
private static List<LlmSkillRecommendation> GetRuleBasedSkillRecommendations(
642+
string taskDescription,
643+
string importance,
644+
string urgency,
645+
TimeSpan inactiveDuration,
646+
int maxSkills)
647+
{
648+
var result = new List<LlmSkillRecommendation>();
649+
bool highImportance = string.Equals(importance, "High", StringComparison.OrdinalIgnoreCase);
650+
bool highUrgency = string.Equals(urgency, "High", StringComparison.OrdinalIgnoreCase);
651+
bool veryStale = inactiveDuration >= TimeSpan.FromDays(3);
652+
bool longText = !string.IsNullOrWhiteSpace(taskDescription) && taskDescription.Length > 40;
653+
654+
if (longText || veryStale)
655+
{
656+
result.Add(new LlmSkillRecommendation
657+
{
658+
SkillId = "decompose",
659+
Title = "任务分解",
660+
Why = "当前任务复杂或停滞,先拆小更容易推进。",
661+
NextStep = "拆成2-3个30分钟内可完成的子任务。",
662+
Confidence = 0.78
663+
});
664+
}
665+
666+
if (highImportance && highUrgency)
667+
{
668+
result.Add(new LlmSkillRecommendation
669+
{
670+
SkillId = "focus_sprint",
671+
Title = "专注冲刺",
672+
Why = "高价值高时效任务适合短时冲刺推进。",
673+
NextStep = "立刻开始一个25分钟专注块,仅做此任务。",
674+
Confidence = 0.82
675+
});
676+
}
677+
else if (!highImportance && highUrgency)
678+
{
679+
result.Add(new LlmSkillRecommendation
680+
{
681+
SkillId = "delegate_prepare",
682+
Title = "委托准备",
683+
Why = "紧急但低重要任务可优先考虑委托。",
684+
NextStep = "写一条委托说明:目标、截止时间、验收标准。",
685+
Confidence = 0.72
686+
});
687+
}
688+
else
689+
{
690+
result.Add(new LlmSkillRecommendation
691+
{
692+
SkillId = "priority_rebalance",
693+
Title = "优先级重排",
694+
Why = "通过重排顺序减少低价值任务挤占时间。",
695+
NextStep = "确认该任务本周优先级,不匹配则延期。",
696+
Confidence = 0.68
697+
});
698+
}
699+
700+
result.Add(new LlmSkillRecommendation
701+
{
702+
SkillId = "risk_check",
703+
Title = "风险检查",
704+
Why = "提前识别阻塞点能降低返工成本。",
705+
NextStep = "写下1个可能阻塞因素和1个应对动作。",
706+
Confidence = 0.64
707+
});
708+
709+
return result
710+
.Where(s => IsAllowedSkillId(s.SkillId))
711+
.GroupBy(s => s.SkillId)
712+
.Select(g => g.First())
713+
.Take(Math.Max(1, maxSkills))
714+
.ToList();
715+
}
716+
536717
internal static (DecompositionStatus status, List<string> subtasks) ParseDecompositionResponse(string llmResponse)
537718
{
538719
Console.WriteLine($"Parsing LLM decomposition response: \"{llmResponse}\"");

MainWindow.xaml.cs

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,13 +331,15 @@ public partial class MainWindow : Window
331331
private bool _proactiveAssistEnabled = true;
332332
private bool _behaviorLearningEnabled = true;
333333
private bool _stuckNudgesEnabled = true;
334+
private bool _llmSkillAssistEnabled = true;
334335
private int _quietHoursStart = 22;
335336
private int _quietHoursEnd = 8;
336337
private const int DailyReminderLimit = 3;
337338
private static readonly TimeSpan ReminderCooldown = TimeSpan.FromMinutes(45);
338339
private int _remindersShownToday = 0;
339340
private DateTime _reminderCounterDate = DateTime.Today;
340341
private readonly Dictionary<string, TaskInteractionState> _taskInteractionStates = new Dictionary<string, TaskInteractionState>();
342+
private readonly Dictionary<string, List<string>> _pendingReminderSkillIds = new Dictionary<string, List<string>>();
341343
private const int DefaultDailyStuckNudgeLimit = 2;
342344
private static readonly TimeSpan StuckNudgeCooldown = TimeSpan.FromHours(2);
343345
private static readonly TimeSpan DefaultStuckNoProgressThreshold = TimeSpan.FromMinutes(90);
@@ -656,6 +658,7 @@ private void LoadProactiveConfig()
656658
_proactiveAssistEnabled = GetAppSettingBool("ProactiveAssistEnabled", true);
657659
_behaviorLearningEnabled = GetAppSettingBool("BehaviorLearningEnabled", true);
658660
_stuckNudgesEnabled = GetAppSettingBool("StuckNudgesEnabled", true);
661+
_llmSkillAssistEnabled = GetAppSettingBool("LlmSkillAssistEnabled", true);
659662
_quietHoursStart = GetAppSettingInt("QuietHoursStart", 22, 0, 23);
660663
_quietHoursEnd = GetAppSettingInt("QuietHoursEnd", 8, 0, 23);
661664
}
@@ -860,9 +863,31 @@ private async void ShowFriendlyReminder(ItemGrid task, string message)
860863
TimeSpan inactiveDuration = DateTime.Now - task.LastProgressDate;
861864
string reminderContext = ShouldRecordBehavior() ? _userProfileManager.BuildReminderContext(task, inactiveDuration) : null;
862865
var (reminder, suggestions) = await _llmService.GenerateTaskReminderAsync(task.Task, taskAge, reminderContext);
866+
var mergedSuggestions = new List<string>(suggestions ?? new List<string>());
867+
868+
if (_llmSkillAssistEnabled)
869+
{
870+
try
871+
{
872+
var skillRecs = await _llmService.RecommendTaskSkillsAsync(
873+
task.Task,
874+
task.Importance,
875+
task.Urgency,
876+
inactiveDuration,
877+
reminderContext,
878+
2);
879+
var shownSkillIds = new List<string>();
880+
mergedSuggestions = MergeSkillSuggestions(mergedSuggestions, skillRecs, shownSkillIds);
881+
RememberPendingReminderSkills(task, shownSkillIds);
882+
}
883+
catch (Exception skillEx)
884+
{
885+
Console.WriteLine($"Skill recommendation failed: {skillEx.Message}");
886+
}
887+
}
863888

864889
// Show modern reminder window
865-
var reminderWindow = new TaskReminderWindow(task, reminder ?? message, suggestions)
890+
var reminderWindow = new TaskReminderWindow(task, reminder ?? message, mergedSuggestions)
866891
{
867892
Owner = this
868893
};
@@ -872,6 +897,10 @@ private async void ShowFriendlyReminder(ItemGrid task, string message)
872897
{
873898
await HandleTaskReminderResult(task, reminderWindow.Result);
874899
}
900+
else
901+
{
902+
await HandleTaskReminderResult(task, TaskReminderResult.Dismissed);
903+
}
875904
}
876905
catch (Exception ex)
877906
{
@@ -945,6 +974,7 @@ private async Task HandleTaskReminderResult(ItemGrid task, TaskReminderResult re
945974
break;
946975
}
947976

977+
ResolveReminderSkillFeedback(task, result);
948978
RecordReminderResultProfile(task, result);
949979

950980
// Save changes to CSV
@@ -1140,6 +1170,91 @@ private void UpdateAdaptiveNudgeParameters(bool force = false)
11401170
}
11411171
}
11421172

1173+
private List<string> MergeSkillSuggestions(List<string> baseSuggestions, List<LlmSkillRecommendation> skills)
1174+
{
1175+
return MergeSkillSuggestions(baseSuggestions, skills, null);
1176+
}
1177+
1178+
private List<string> MergeSkillSuggestions(List<string> baseSuggestions, List<LlmSkillRecommendation> skills, List<string> shownSkillIds)
1179+
{
1180+
var result = baseSuggestions ?? new List<string>();
1181+
if (skills == null || skills.Count == 0)
1182+
{
1183+
return result;
1184+
}
1185+
1186+
foreach (var skill in skills)
1187+
{
1188+
if (skill == null) continue;
1189+
string text = $"Skill[{skill.Title}]: {skill.NextStep}";
1190+
if (!result.Any(s => string.Equals(s, text, StringComparison.OrdinalIgnoreCase)))
1191+
{
1192+
result.Insert(0, text);
1193+
}
1194+
if (shownSkillIds != null && !string.IsNullOrWhiteSpace(skill.SkillId))
1195+
{
1196+
string id = skill.SkillId.Trim().ToLowerInvariant();
1197+
if (!shownSkillIds.Contains(id))
1198+
{
1199+
shownSkillIds.Add(id);
1200+
RecordSuggestionShownProfile(id);
1201+
}
1202+
}
1203+
}
1204+
return result.Take(5).ToList();
1205+
}
1206+
1207+
private void RememberPendingReminderSkills(ItemGrid task, List<string> shownSkillIds)
1208+
{
1209+
if (task == null) return;
1210+
string key = GetTaskTrackingKey(task);
1211+
if (shownSkillIds == null || shownSkillIds.Count == 0)
1212+
{
1213+
_pendingReminderSkillIds.Remove(key);
1214+
return;
1215+
}
1216+
_pendingReminderSkillIds[key] = shownSkillIds;
1217+
}
1218+
1219+
private void ResolveReminderSkillFeedback(ItemGrid task, TaskReminderResult result)
1220+
{
1221+
if (task == null) return;
1222+
string key = GetTaskTrackingKey(task);
1223+
if (!_pendingReminderSkillIds.TryGetValue(key, out var ids) || ids == null || ids.Count == 0)
1224+
{
1225+
return;
1226+
}
1227+
1228+
string feedbackType = null;
1229+
string targetSkillId = ids[0];
1230+
1231+
switch (result)
1232+
{
1233+
case TaskReminderResult.Decompose:
1234+
feedbackType = "accepted";
1235+
targetSkillId = ids.FirstOrDefault(id => string.Equals(id, "decompose", StringComparison.OrdinalIgnoreCase)) ?? targetSkillId;
1236+
break;
1237+
case TaskReminderResult.Completed:
1238+
case TaskReminderResult.Updated:
1239+
feedbackType = "accepted";
1240+
break;
1241+
case TaskReminderResult.Snoozed:
1242+
feedbackType = "deferred";
1243+
break;
1244+
case TaskReminderResult.Dismissed:
1245+
default:
1246+
feedbackType = "rejected";
1247+
break;
1248+
}
1249+
1250+
if (!string.IsNullOrWhiteSpace(feedbackType) && !string.IsNullOrWhiteSpace(targetSkillId))
1251+
{
1252+
RecordSuggestionFeedbackProfile(targetSkillId, feedbackType);
1253+
}
1254+
1255+
_pendingReminderSkillIds.Remove(key);
1256+
}
1257+
11431258
private static TimeSpan ClampThreshold(TimeSpan threshold)
11441259
{
11451260
if (threshold < MinStuckNoProgressThreshold)

ReminderSettingsWindow.xaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@
197197
Margin="0,4"/>
198198
<CheckBox Content="启用卡点轻提醒"
199199
IsChecked="{Binding StuckNudgesEnabled}"
200+
Margin="0,4"/>
201+
<CheckBox Content="启用LLM技能建议"
202+
IsChecked="{Binding LlmSkillAssistEnabled}"
200203
Margin="0,4,0,10"/>
201204

202205
<Grid>

0 commit comments

Comments
 (0)