에이전트 루프와 코드 언어 지원, PPT 생성 품질을 함께 고도화

- AgentCommandQueue를 도입해 실행 중 추가 입력을 우선순위와 인터럽트 여부까지 포함해 처리하도록 정리함
- AgentToolResultBudget와 AgentQueryContextBuilder에 tool result preview 캐시를 연결해 긴 세션에서 축약 결과 재사용을 안정화함
- CodeLanguageCatalog를 추가해 코드 탭의 내장 언어 지원, 인덱싱 확장자, 시스템 프롬프트 언어 가이드, LSP 언어 판정을 한 카탈로그로 통합함
- 설정의 코드 탭에 지원 언어(LSP)와 코드 탭 기본 지원 언어를 명시적으로 표시하도록 보강함
- DocumentPlannerTool의 presentation 구조를 컨설팅형 스토리라인으로 정리하고, PptxSkill에 executive_summary/recommendation/roadmap/comparison/kpi_dashboard 레이아웃을 추가함
- pptx-creator 스킬을 AX native pptx_create 중심으로 재작성하고, 관련 회귀 테스트를 추가했으며 WorkspaceContextGeneratorTests의 nullable 경고도 정리함

검증 결과
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_impl\\ -p:IntermediateOutputPath=obj\\verify_impl\\
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeLanguageCatalogTests|AgentCommandQueueTests|AgentToolResultBudgetTests|DocumentPlannerPresentationTests|PptxSkillConsultingDeckTests" -p:OutputPath=bin\\verify_impl_tests\\ -p:IntermediateOutputPath=obj\\verify_impl_tests\\
This commit is contained in:
2026-04-14 19:53:39 +09:00
parent 946c31e275
commit 0b6d60e959
23 changed files with 1837 additions and 746 deletions

View File

@@ -0,0 +1,34 @@
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class AgentCommandQueueTests
{
[Fact]
public void DrainAll_ShouldRespectPriorityThenSequence()
{
var queue = new AgentCommandQueue();
queue.EnqueuePrompt("later", "later");
queue.EnqueuePrompt("next", "next");
queue.EnqueuePrompt("now", "now", requestInterrupt: true);
var drained = queue.DrainAll();
drained.Select(x => x.Content).Should().Equal("now", "next", "later");
drained[0].RequestInterrupt.Should().BeTrue();
}
[Fact]
public void Clear_ShouldRemoveQueuedItems()
{
var queue = new AgentCommandQueue();
queue.EnqueuePrompt("first");
queue.EnqueueNotification("note");
queue.Clear();
queue.DrainAll().Should().BeEmpty();
}
}

View File

@@ -0,0 +1,58 @@
using AxCopilot.Models;
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class AgentToolResultBudgetTests
{
[Fact]
public void Apply_ShouldPersistPreviewToSourceAndReuseItOnNextQueryView()
{
var longContent = new string('A', 1400);
var sourceMessages = new List<ChatMessage>
{
new()
{
MsgId = "tool-1",
Role = "user",
Content = $$"""{"type":"tool_result","tool_use_id":"call-1","tool_name":"file_read","content":"{{longContent}}"}"""
},
new()
{
MsgId = "tail-1",
Role = "assistant",
Content = "recent tail"
}
};
var firstWindow = sourceMessages.Select(message => new ChatMessage
{
MsgId = message.MsgId,
Role = message.Role,
Content = message.Content,
QueryPreviewContent = message.QueryPreviewContent,
Timestamp = message.Timestamp
}).ToList();
var first = AgentToolResultBudget.Apply(firstWindow, protectedRecentNonSystemMessages: 1, sourceMessages: sourceMessages);
sourceMessages[0].QueryPreviewContent.Should().NotBeNullOrWhiteSpace();
first.TruncatedCount.Should().Be(1);
var secondWindow = sourceMessages.Select(message => new ChatMessage
{
MsgId = message.MsgId,
Role = message.Role,
Content = message.Content,
QueryPreviewContent = message.QueryPreviewContent,
Timestamp = message.Timestamp
}).ToList();
var second = AgentToolResultBudget.Apply(secondWindow, protectedRecentNonSystemMessages: 1, sourceMessages: sourceMessages);
second.ReusedPreviewCount.Should().Be(1);
secondWindow[0].Content.Should().Be(sourceMessages[0].QueryPreviewContent);
}
}

View File

@@ -0,0 +1,38 @@
using AxCopilot.Services;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class CodeLanguageCatalogTests
{
[Theory]
[InlineData(".cs", "csharp")]
[InlineData(".py", "python")]
[InlineData(".java", "java")]
[InlineData(".go", null)]
[InlineData(".unknown", null)]
public void DetectLspLanguageId_ShouldMatchCatalog(string extension, string? expected)
{
CodeLanguageCatalog.DetectLspLanguageId($"sample{extension}").Should().Be(expected);
}
[Theory]
[InlineData(".go", true)]
[InlineData(".rs", true)]
[InlineData(".sql", true)]
[InlineData(".foo", false)]
public void IsCodeLikeFile_ShouldReflectBuiltInSupport(string extension, bool expected)
{
CodeLanguageCatalog.IsCodeLikeFile(extension).Should().Be(expected);
}
[Fact]
public void SupportDescriptions_ShouldContainBroadStaticSupportAndFocusedLspSupport()
{
CodeLanguageCatalog.BuildStaticSupportDescription().Should().Contain("Go");
CodeLanguageCatalog.BuildStaticSupportDescription().Should().Contain("Rust");
CodeLanguageCatalog.BuildLspSupportDescription().Should().Contain("C# (.NET)");
CodeLanguageCatalog.BuildLspSupportDescription().Should().NotContain("Go");
}
}

View File

@@ -0,0 +1,40 @@
using System.IO;
using System.Text.Json;
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class DocumentPlannerPresentationTests
{
[Fact]
public async Task ExecuteAsync_ForPresentation_ShouldReturnConsultingStoryline()
{
var tool = new DocumentPlannerTool();
var context = new AgentContext
{
WorkFolder = Path.GetTempPath(),
Permission = "Auto",
OperationMode = "external",
};
var args = JsonDocument.Parse(
"""
{
"topic": "2026 사업 전략 제안",
"document_type": "presentation",
"target_pages": 6
}
""").RootElement;
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
result.Success.Should().BeTrue();
result.Output.Should().Contain("Executive Summary");
result.Output.Should().Contain("Situation & Imperative");
result.Output.Should().Contain("Options & Recommendation");
result.Output.Should().Contain("Implementation Roadmap");
result.Output.Should().Contain("Impact & Ask");
}
}

View File

@@ -0,0 +1,118 @@
using System.IO;
using System.Text.Json;
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class PptxSkillConsultingDeckTests
{
[Fact]
public async Task ExecuteAsync_ShouldCreateDeck_WithConsultingLayouts()
{
var workDir = Path.Combine(Path.GetTempPath(), "ax-pptx-consulting-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(workDir);
try
{
var context = new AgentContext
{
WorkFolder = workDir,
Permission = "Auto",
OperationMode = "external",
};
var tool = new PptxSkill();
var args = JsonDocument.Parse(
"""
{
"path": "consulting-deck.pptx",
"theme": "professional",
"slides": [
{
"layout": "executive_summary",
"title": "Executive Summary",
"headline": "핵심 투자 우선순위를 재배치하면 두 개 분기 내 수익성을 개선할 수 있습니다.",
"summary_points": [
"유지율 개선이 신규 확보보다 높은 수익 효과를 보입니다.",
"상위 3개 비용 항목에 구조적 비효율이 확인되었습니다.",
"즉시 실행 가능한 과제와 중기 투자 과제를 분리해야 합니다."
],
"recommendation": "CRM 고도화와 운영 자동화를 병행하고 저효율 캠페인은 축소합니다.",
"kpis": [
{ "label": "매출총이익", "value": "+4.2%p", "trend": "2Q forecast", "note": "product mix 개선" },
{ "label": "CAC", "value": "-11%", "trend": "target", "note": "퍼널 최적화" },
{ "label": "Retention", "value": "+6pt", "trend": "12M", "note": "핵심 세그먼트" }
]
},
{
"layout": "comparison",
"title": "Options",
"headline": "균형형 대안이 실행 속도와 확장성의 균형이 가장 좋습니다.",
"options": [
{ "name": "Option A", "pros": "빠른 적용", "cons": "확장성 제한", "verdict": "Fastest" },
{ "name": "Option B", "pros": "균형 잡힌 투자", "cons": "의사결정 필요", "verdict": "Recommended" },
{ "name": "Option C", "pros": "장기 최적화", "cons": "리드타임 길음", "verdict": "Strategic" }
]
},
{
"layout": "roadmap",
"title": "Roadmap",
"phases": [
{ "title": "Phase 1", "detail": "진단 및 설계", "timeline": "0-30일", "owner": "PM" },
{ "title": "Phase 2", "detail": "구현 및 검증", "timeline": "30-60일", "owner": "Delivery" },
{ "title": "Phase 3", "detail": "확산 및 운영정착", "timeline": "60-90일", "owner": "Business" }
]
},
{
"layout": "kpi_dashboard",
"title": "KPI Dashboard",
"summary_points": [
"비용 구조와 유지율 개선이 동시에 진행될 때 목표 이익률 달성이 가능합니다."
],
"kpis": [
{ "label": "Revenue", "value": "12%", "trend": "YoY", "note": "Strong" },
{ "label": "Cost", "value": "-8%", "trend": "vs plan", "note": "Improving" },
{ "label": "NPS", "value": "61", "trend": "survey", "note": "Stable" },
{ "label": "Delivery", "value": "94%", "trend": "SLA", "note": "On track" }
]
},
{
"layout": "recommendation",
"title": "Recommendation",
"recommendation": "고객가치가 높은 세그먼트에 집중하고 운영 자동화로 비용을 즉시 절감합니다.",
"summary_points": [
"저효율 캠페인 예산을 재배치할 수 있습니다.",
"고객 이탈 구간을 CRM 자동화로 줄일 수 있습니다.",
"성과 확인까지의 시간이 상대적으로 짧습니다."
],
"next_steps": [
"2주 내 우선순위 확정",
"6주 내 자동화 MVP 구축",
"분기 말 KPI 리뷰"
]
}
]
}
""").RootElement;
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
result.Success.Should().BeTrue();
File.Exists(Path.Combine(workDir, "consulting-deck.pptx")).Should().BeTrue();
result.Output.Should().Contain("PPTX");
}
finally
{
try
{
if (Directory.Exists(workDir))
Directory.Delete(workDir, true);
}
catch
{
}
}
}
}

View File

@@ -73,7 +73,7 @@ public class WorkspaceContextGeneratorTests
var result = WorkspaceContextGenerator.LoadContext(tempDir);
result.Should().NotBeNull();
result!.Should().Contain("(truncated)");
result.Length.Should().BeLessOrEqualTo(4100); // 4000 + truncation message
result!.Length.Should().BeLessOrEqualTo(4100); // 4000 + truncation message
}
finally
{