using System.Text.RegularExpressions; namespace AxCopilot.Services.Agent; /// /// LLM 응답 텍스트에서 작업 계획(단계 목록)을 추출합니다. /// Plan-and-Solve 논문의 경량 구현: 번호가 매겨진 단계를 파싱하여 진행률 추적에 사용합니다. /// public static class TaskDecomposer { // 번호 매긴 단계 패턴: "1. ...", "1) ...", "Step 1: ..." private static readonly Regex StepPattern = new( @"(?:^|\n)\s*(?:(?:Step\s*)?(\d+)[.):\-]\s*)(.+?)(?=\n|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// /// LLM 텍스트 응답에서 계획 단계를 추출합니다. /// /// 단계 목록. 2개 미만이면 빈 리스트 (계획이 아닌 것으로 판단). public static List ExtractSteps(string text) { if (string.IsNullOrWhiteSpace(text)) return []; var matches = StepPattern.Matches(text); if (matches.Count < 2) return []; var steps = new List(); int lastNum = 0; foreach (Match m in matches) { if (int.TryParse(m.Groups[1].Value, out var num)) { // 연속 번호인지 확인 (1,2,3... 또는 첫 번째) if (num == lastNum + 1 || lastNum == 0) { var stepText = m.Groups[2].Value.Trim(); // 마크다운 기호 제거 (볼드, 이탤릭, 코드, 링크 등) stepText = Regex.Replace(stepText, @"\*\*(.+?)\*\*", "$1"); // **볼드** stepText = Regex.Replace(stepText, @"\*(.+?)\*", "$1"); // *이탤릭* stepText = Regex.Replace(stepText, @"`(.+?)`", "$1"); // `코드` stepText = Regex.Replace(stepText, @"\[(.+?)\]\(.+?\)", "$1"); // [링크](url) stepText = stepText.TrimEnd(':', ' '); // 너무 짧거나 긴 것은 제외 if (stepText.Length >= 3 && stepText.Length <= 300) { steps.Add(stepText); lastNum = num; } } } } // 최소 2단계, 최대 20단계 return steps.Count >= 2 ? steps.Take(20).ToList() : []; } /// /// 도구 호출과 매칭하여 현재 어느 단계를 실행 중인지 추정합니다. /// public static int EstimateCurrentStep(List steps, string toolName, string toolSummary, int lastStep) { if (steps.Count == 0) return 0; // 다음 단계부터 검색 (역행하지 않음) for (int i = lastStep; i < steps.Count; i++) { var step = steps[i].ToLowerInvariant(); var tool = toolName.ToLowerInvariant(); var summary = toolSummary.ToLowerInvariant(); // 단계 텍스트에 도구명이나 주요 키워드가 포함되면 매칭 if (step.Contains(tool) || ContainsKeywordOverlap(step, summary)) { return i; } } // 매칭 실패 → 마지막 단계 + 1로 전진 (최소 진행) return Math.Min(lastStep + 1, steps.Count - 1); } private static bool ContainsKeywordOverlap(string stepText, string summary) { if (string.IsNullOrEmpty(summary)) return false; // 의미 있는 키워드 추출 (3자 이상 단어) var summaryWords = summary.Split(' ', '/', '\\', '.', ',', ':', ';', '(', ')') .Where(w => w.Length >= 3) .Take(5); return summaryWords.Any(w => stepText.Contains(w)); } }