에이전트 루프와 코드 언어 지원, 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:
34
src/AxCopilot.Tests/Services/AgentCommandQueueTests.cs
Normal file
34
src/AxCopilot.Tests/Services/AgentCommandQueueTests.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
58
src/AxCopilot.Tests/Services/AgentToolResultBudgetTests.cs
Normal file
58
src/AxCopilot.Tests/Services/AgentToolResultBudgetTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
38
src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs
Normal file
38
src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
118
src/AxCopilot.Tests/Services/PptxSkillConsultingDeckTests.cs
Normal file
118
src/AxCopilot.Tests/Services/PptxSkillConsultingDeckTests.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user