Files
AX-Copilot-Codex/src/AxCopilot/Services/Agent/TaskDecomposer.cs

96 lines
3.7 KiB
C#

using System.Text.RegularExpressions;
namespace AxCopilot.Services.Agent;
/// <summary>
/// LLM 응답 텍스트에서 작업 계획(단계 목록)을 추출합니다.
/// Plan-and-Solve 논문의 경량 구현: 번호가 매겨진 단계를 파싱하여 진행률 추적에 사용합니다.
/// </summary>
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);
/// <summary>
/// LLM 텍스트 응답에서 계획 단계를 추출합니다.
/// </summary>
/// <returns>단계 목록. 2개 미만이면 빈 리스트 (계획이 아닌 것으로 판단).</returns>
public static List<string> ExtractSteps(string text)
{
if (string.IsNullOrWhiteSpace(text)) return [];
var matches = StepPattern.Matches(text);
if (matches.Count < 2) return [];
var steps = new List<string>();
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() : [];
}
/// <summary>
/// 도구 호출과 매칭하여 현재 어느 단계를 실행 중인지 추정합니다.
/// </summary>
public static int EstimateCurrentStep(List<string> 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));
}
}