@@ -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 \n User 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 } \" ") ;
0 commit comments