96 lines
3.7 KiB
C#
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));
|
|
}
|
|
}
|