diff --git a/README.md b/README.md index ee538ee..dd1ab44 100644 --- a/README.md +++ b/README.md @@ -1917,3 +1917,11 @@ MIT License - 테스트로 [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs), [ArtifactRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs), [DeckRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckRepairGuideServiceTests.cs), [DocxSkillTemplateFeaturesTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocxSkillTemplateFeaturesTests.cs)를 확장했습니다. - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_repair_finalize\\ -p:IntermediateOutputPath=obj\\verify_doc_repair_finalize\\` 경고 0 / 오류 0 - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|ArtifactRepairGuideServiceTests|DeckRepairGuideServiceTests|DocxSkillTemplateFeaturesTests" -p:OutputPath=bin\\verify_doc_repair_finalize_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_repair_finalize_tests\\` 통과 11 + +업데이트: 2026-04-15 09:24 (KST) +- 에이전틱 루프의 큐 소비 책임을 더 잘게 분리했습니다. [AgentQueuedCommandProjector.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentQueuedCommandProjector.cs)를 추가해 queued command 배치를 `대화 메시지 + 이벤트`로 투영하는 로직을 별도 helper로 옮겼고, [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)는 투영 결과를 적용하는 얇은 orchestration 역할에 더 가깝게 정리했습니다. +- workbook/dashboard 리뷰도 더 엄격해졌습니다. [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)는 dashboard sheet가 있어도 KPI·trend·decision 정보가 비어 있으면 얇은 dashboard로 경고하고, [ArtifactRepairGuideService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs)는 이를 `core story`가 보이도록 KPI/decision 블록을 추가하라는 문장으로 바꿉니다. +- PPT deck 리뷰는 storyline 힌트까지 검사합니다. [DeckQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs)는 storyline에 `Options`, `Roadmap`, `Appendix`가 있는데 실제 슬라이드가 빠진 경우 별도 이슈를 만들고, [DeckRepairGuideService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs)는 이를 deck storyline 보강 액션으로 연결합니다. +- 테스트로 [AgentQueuedCommandProjectorTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentQueuedCommandProjectorTests.cs), [DeckQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs), [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs), [ArtifactRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs), [DeckRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckRepairGuideServiceTests.cs)를 확장했습니다. +- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_loop_doc_finish2\\ -p:IntermediateOutputPath=obj\\verify_loop_doc_finish2\\` 경고 0 / 오류 0 +- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentQueuedCommandProjectorTests|AgentCommandQueueTests|ArtifactQualityReviewServiceTests|ArtifactRepairGuideServiceTests|DeckQualityReviewServiceTests|DeckRepairGuideServiceTests|PptxSkillGoldenDeckTests|ExcelSkillDashboardSummaryTests" -p:OutputPath=bin\\verify_loop_doc_finish2_tests\\ -p:IntermediateOutputPath=obj\\verify_loop_doc_finish2_tests\\` 통과 25 diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 9b4056b..f9870f4 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -988,3 +988,11 @@ UI ?붿옄???€洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾 - 테스트는 [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs), [ArtifactRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs), [DeckRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckRepairGuideServiceTests.cs), [DocxSkillTemplateFeaturesTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocxSkillTemplateFeaturesTests.cs)를 확장해 회귀를 고정했습니다. - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_repair_finalize\\ -p:IntermediateOutputPath=obj\\verify_doc_repair_finalize\\` 경고 0 / 오류 0 - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|ArtifactRepairGuideServiceTests|DeckRepairGuideServiceTests|DocxSkillTemplateFeaturesTests" -p:OutputPath=bin\\verify_doc_repair_finalize_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_repair_finalize_tests\\` 통과 11 + +업데이트: 2026-04-15 09:24 (KST) +- 에이전틱 루프의 queued command 소비 로직을 helper로 분리했습니다. [AgentQueuedCommandProjector.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentQueuedCommandProjector.cs)는 drain된 큐 배치를 `queued_input_interrupt`, `queue_notification`, `queue_resume`, `queued_prompt` 같은 대화 메시지와 thinking/user 이벤트로 투영합니다. [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)는 이 결과를 적용하는 역할만 남겨 루프 본체의 책임을 더 줄였습니다. +- workbook/dashboard 품질 리뷰도 강화했습니다. [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)는 dashboard sheet가 있어도 KPI·trend·decision 내용이 부족하면 별도 이슈를 만들고, [ArtifactRepairGuideService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs)는 이를 `core story`가 보이도록 KPI/decision 블록을 추가하라는 가이드로 연결합니다. +- deck 품질 리뷰는 storyline 힌트까지 보기 시작했습니다. [DeckQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs)는 storyline에 `Options`, `Roadmap`, `Appendix`가 있는데 실제 슬라이드가 빠진 경우 별도 이슈를 만들고, [DeckRepairGuideService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs)는 이를 storyline 보강 액션으로 바꿉니다. +- 테스트는 [AgentQueuedCommandProjectorTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentQueuedCommandProjectorTests.cs), [DeckQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs), [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs), [ArtifactRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs), [DeckRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckRepairGuideServiceTests.cs)를 확장해 회귀를 고정했습니다. +- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_loop_doc_finish2\\ -p:IntermediateOutputPath=obj\\verify_loop_doc_finish2\\` 경고 0 / 오류 0 +- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentQueuedCommandProjectorTests|AgentCommandQueueTests|ArtifactQualityReviewServiceTests|ArtifactRepairGuideServiceTests|DeckQualityReviewServiceTests|DeckRepairGuideServiceTests|PptxSkillGoldenDeckTests|ExcelSkillDashboardSummaryTests" -p:OutputPath=bin\\verify_loop_doc_finish2_tests\\ -p:IntermediateOutputPath=obj\\verify_loop_doc_finish2_tests\\` 통과 25 diff --git a/src/AxCopilot.Tests/Services/AgentQueuedCommandProjectorTests.cs b/src/AxCopilot.Tests/Services/AgentQueuedCommandProjectorTests.cs new file mode 100644 index 0000000..839aaa7 --- /dev/null +++ b/src/AxCopilot.Tests/Services/AgentQueuedCommandProjectorTests.cs @@ -0,0 +1,71 @@ +using AxCopilot.Models; +using AxCopilot.Services.Agent; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class AgentQueuedCommandProjectorTests +{ + [Fact] + public void Project_ShouldCreateInterruptMessageAndDeferredEvent() + { + var now = DateTime.Now; + var commands = new List + { + new(1, AgentCommandKind.Steering, AgentQueuePriority.Now, "steer-now", now, true), + new(2, AgentCommandKind.Notification, AgentQueuePriority.Now, "later-note", now.AddSeconds(1), false), + }; + + var projection = AgentQueuedCommandProjector.Project(commands, deferredCount: 2); + + projection.Messages.Should().ContainSingle(message => + message.MetaKind == "queued_input_interrupt" && + message.Role == "system"); + projection.Messages.Should().Contain(message => + message.MetaKind == "queued_steering" && + message.Role == "user" && + message.Content == "steer-now"); + projection.Messages.Should().Contain(message => + message.MetaKind == "queue_notification" && + message.Role == "system" && + message.Content == "later-note"); + projection.Events.Should().Contain(evt => + evt.Type == AgentEventType.Thinking && + evt.Summary.Contains("Deferred 2 lower-priority", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void Project_ShouldMapPermissionResumeAndPromptKindsConsistently() + { + var now = DateTime.Now; + var commands = new List + { + new(1, AgentCommandKind.PermissionContinuation, AgentQueuePriority.Now, "continue-tool", now, true), + new(2, AgentCommandKind.Resume, AgentQueuePriority.Now, "resume-run", now.AddSeconds(1), false), + new(3, AgentCommandKind.Prompt, AgentQueuePriority.Now, "follow-up", now.AddSeconds(2), false), + new(4, AgentCommandKind.UserDecision, AgentQueuePriority.Now, "choose-option-b", now.AddSeconds(3), false), + }; + + var projection = AgentQueuedCommandProjector.Project(commands, deferredCount: 0); + + projection.Messages.Should().Contain(message => + message.MetaKind == "queue_permission_continuation" && + message.Role == "system"); + projection.Messages.Should().Contain(message => + message.MetaKind == "queue_resume" && + message.Role == "system"); + projection.Messages.Should().Contain(message => + message.MetaKind == "queued_prompt" && + message.Role == "user"); + projection.Messages.Should().Contain(message => + message.MetaKind == "queued_user_decision" && + message.Role == "user"); + projection.Events.Should().Contain(evt => + evt.Type == AgentEventType.UserMessage && + evt.Summary == "follow-up"); + projection.Events.Should().Contain(evt => + evt.Type == AgentEventType.Thinking && + evt.Summary == "continue-tool"); + } +} diff --git a/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs b/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs index a3e6950..8d239ae 100644 --- a/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs +++ b/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs @@ -106,5 +106,6 @@ public class ArtifactQualityReviewServiceTests review.Issues.Should().Contain(issue => issue.Message.Contains("highlights or actions", StringComparison.OrdinalIgnoreCase)); review.Issues.Should().Contain(issue => issue.Message.Contains("supporting detail sheets", StringComparison.OrdinalIgnoreCase)); review.Issues.Should().Contain(issue => issue.Message.Contains("trend or variance formulas", StringComparison.OrdinalIgnoreCase)); + review.Issues.Should().Contain(issue => issue.Message.Contains("KPI, trend, or decision content", StringComparison.OrdinalIgnoreCase)); } } diff --git a/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs b/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs index 0dea5c3..198f9f2 100644 --- a/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs +++ b/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs @@ -14,6 +14,7 @@ public class ArtifactRepairGuideServiceTests 68, ["Includes summary sheet"], [ + new ArtifactReviewIssue("Dashboard sheet lacks KPI, trend, or decision content.", ArtifactReviewSeverity.Info), new ArtifactReviewIssue("Workbook could benefit from a dashboard sheet to summarize multi-sheet trends.", ArtifactReviewSeverity.Info), new ArtifactReviewIssue("Summary sheet does not link to detail sheets.", ArtifactReviewSeverity.Warning) ]); @@ -21,6 +22,7 @@ public class ArtifactRepairGuideServiceTests var guide = ArtifactRepairGuideService.BuildGuide(review); guide.Should().Contain("dashboard sheet"); + guide.Should().Contain("core story"); guide.Should().Contain("detail sheets"); } diff --git a/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs b/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs index eb9aacd..54bc339 100644 --- a/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs +++ b/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs @@ -83,4 +83,28 @@ public class DeckQualityReviewServiceTests review.Issues.Should().Contain(issue => issue.Message.Contains("headline is too long")); review.Issues.Should().Contain(issue => issue.Message.Contains("comparison slide needs at least two options")); } + + [Fact] + public void ReviewDeck_ShouldFlagMissingStorylineSlides_WhenHintsAreProvided() + { + using var slides = JsonDocument.Parse( + """ + [ + { "layout": "title", "title": "PMO Steering" }, + { "layout": "executive_summary", "title": "Executive Summary", "headline": "Message", "summary_points": ["A", "B"], "recommendation": "Proceed" }, + { "layout": "recommendation", "title": "Recommendation", "recommendation": "Proceed", "summary_points": ["Reason"] } + ] + """); + + var review = DeckQualityReviewService.ReviewDeck( + "Storyline Deck", + slides.RootElement, + hasTemplate: false, + autoRepairCount: 0, + storyline: ["Executive Summary", "Options", "Roadmap", "Appendix"]); + + review.Issues.Should().Contain(issue => issue.Message.Contains("Storyline expects an options or comparison slide", StringComparison.OrdinalIgnoreCase)); + review.Issues.Should().Contain(issue => issue.Message.Contains("Storyline expects a roadmap slide", StringComparison.OrdinalIgnoreCase)); + review.Issues.Should().Contain(issue => issue.Message.Contains("Storyline expects appendix or evidence support", StringComparison.OrdinalIgnoreCase)); + } } diff --git a/src/AxCopilot.Tests/Services/DeckRepairGuideServiceTests.cs b/src/AxCopilot.Tests/Services/DeckRepairGuideServiceTests.cs index 3bee15b..fa0087f 100644 --- a/src/AxCopilot.Tests/Services/DeckRepairGuideServiceTests.cs +++ b/src/AxCopilot.Tests/Services/DeckRepairGuideServiceTests.cs @@ -53,4 +53,21 @@ public class DeckRepairGuideServiceTests guide.Should().Contain("appendix"); guide.Should().Contain("distinct message"); } + + [Fact] + public void BuildGuide_ShouldTranslateStorylineGapIntoConcreteAction() + { + var report = new DeckQualityReport( + 63, + [], + [ + new DeckReviewIssue("Storyline expects an options or comparison slide but none is present.", DeckReviewSeverity.Warning) + ], + ["Executive Summary", "Options"]); + + var guide = DeckRepairGuideService.BuildGuide(report); + + guide.Should().Contain("comparison slide"); + guide.Should().Contain("storyline"); + } } diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs index 477829b..039179f 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopService.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs @@ -97,90 +97,10 @@ public partial class AgentLoopService if (drained.Count == 0) return; - var interruptingCommands = drained.Count(x => - x.RequestInterrupt && - x.Kind is AgentCommandKind.Prompt or AgentCommandKind.Steering or AgentCommandKind.UserDecision or AgentCommandKind.PermissionContinuation); - if (interruptingCommands > 0) - { - messages.Add(new ChatMessage - { - Role = "system", - MetaKind = "queued_input_interrupt", - Content = $"[queued input] {interruptingCommands} new instruction(s) arrived during execution. Prioritize the newest user direction before continuing.", - Timestamp = DateTime.Now, - }); - } - - foreach (var item in drained) - { - switch (item.Kind) - { - case AgentCommandKind.Notification: - messages.Add(new ChatMessage - { - Role = "system", - MetaKind = "queue_notification", - Content = item.Content, - Timestamp = item.CreatedAt, - }); - EmitEvent(AgentEventType.Thinking, "", item.Content); - break; - - case AgentCommandKind.PermissionContinuation: - messages.Add(new ChatMessage - { - Role = "system", - MetaKind = "queue_permission_continuation", - Content = item.Content, - Timestamp = item.CreatedAt, - }); - EmitEvent(AgentEventType.Thinking, "", item.Content); - break; - - case AgentCommandKind.Resume: - messages.Add(new ChatMessage - { - Role = "system", - MetaKind = "queue_resume", - Content = item.Content, - Timestamp = item.CreatedAt, - }); - EmitEvent(AgentEventType.Thinking, "", item.Content); - break; - - case AgentCommandKind.Steering: - case AgentCommandKind.UserDecision: - messages.Add(new ChatMessage - { - Role = "user", - MetaKind = item.Kind == AgentCommandKind.Steering ? "queued_steering" : "queued_user_decision", - Content = item.Content, - Timestamp = item.CreatedAt, - }); - EmitEvent(AgentEventType.UserMessage, "", item.Content); - break; - - default: - messages.Add(new ChatMessage - { - Role = "user", - MetaKind = item.RequestInterrupt ? "queued_prompt_interrupt" : "queued_prompt", - Content = item.Content, - Timestamp = item.CreatedAt, - }); - EmitEvent(AgentEventType.UserMessage, "", item.Content); - break; - } - } - - var deferredCount = queuedSnapshot.Count - drained.Count; - if (deferredCount > 0) - { - EmitEvent( - AgentEventType.Thinking, - "", - $"Deferred {deferredCount} lower-priority queued item(s) for a later turn."); - } + var projection = AgentQueuedCommandProjector.Project(drained, queuedSnapshot.Count - drained.Count); + messages.AddRange(projection.Messages); + foreach (var evt in projection.Events) + EmitEvent(evt.Type, evt.ToolName, evt.Summary); } /// 에이전트 이벤트 스트림 (UI 바인딩용). diff --git a/src/AxCopilot/Services/Agent/AgentQueuedCommandProjector.cs b/src/AxCopilot/Services/Agent/AgentQueuedCommandProjector.cs new file mode 100644 index 0000000..0f660df --- /dev/null +++ b/src/AxCopilot/Services/Agent/AgentQueuedCommandProjector.cs @@ -0,0 +1,116 @@ +using AxCopilot.Models; + +namespace AxCopilot.Services.Agent; + +internal sealed record AgentQueuedCommandProjectionEvent( + AgentEventType Type, + string ToolName, + string Summary); + +internal sealed record AgentQueuedCommandProjectionResult( + IReadOnlyList Messages, + IReadOnlyList Events); + +/// +/// 큐에서 꺼낸 명령 배치를 실제 대화 메시지와 이벤트로 투영합니다. +/// AgentLoopService 내부 분기 부담을 줄이고 테스트 가능한 형태로 분리합니다. +/// +internal static class AgentQueuedCommandProjector +{ + public static AgentQueuedCommandProjectionResult Project( + IReadOnlyList drained, + int deferredCount) + { + var messages = new List(); + var events = new List(); + if (drained.Count == 0) + return new AgentQueuedCommandProjectionResult(messages, events); + + var interruptingCommands = drained.Count(x => + x.RequestInterrupt && + x.Kind is AgentCommandKind.Prompt or AgentCommandKind.Steering or AgentCommandKind.UserDecision or AgentCommandKind.PermissionContinuation); + + if (interruptingCommands > 0) + { + messages.Add(new ChatMessage + { + Role = "system", + MetaKind = "queued_input_interrupt", + Content = $"[queued input] {interruptingCommands} new instruction(s) arrived during execution. Prioritize the newest user direction before continuing.", + Timestamp = DateTime.Now, + }); + } + + foreach (var item in drained) + { + switch (item.Kind) + { + case AgentCommandKind.Notification: + messages.Add(new ChatMessage + { + Role = "system", + MetaKind = "queue_notification", + Content = item.Content, + Timestamp = item.CreatedAt, + }); + events.Add(new AgentQueuedCommandProjectionEvent(AgentEventType.Thinking, "", item.Content)); + break; + + case AgentCommandKind.PermissionContinuation: + messages.Add(new ChatMessage + { + Role = "system", + MetaKind = "queue_permission_continuation", + Content = item.Content, + Timestamp = item.CreatedAt, + }); + events.Add(new AgentQueuedCommandProjectionEvent(AgentEventType.Thinking, "", item.Content)); + break; + + case AgentCommandKind.Resume: + messages.Add(new ChatMessage + { + Role = "system", + MetaKind = "queue_resume", + Content = item.Content, + Timestamp = item.CreatedAt, + }); + events.Add(new AgentQueuedCommandProjectionEvent(AgentEventType.Thinking, "", item.Content)); + break; + + case AgentCommandKind.Steering: + case AgentCommandKind.UserDecision: + messages.Add(new ChatMessage + { + Role = "user", + MetaKind = item.Kind == AgentCommandKind.Steering ? "queued_steering" : "queued_user_decision", + Content = item.Content, + Timestamp = item.CreatedAt, + }); + events.Add(new AgentQueuedCommandProjectionEvent(AgentEventType.UserMessage, "", item.Content)); + break; + + default: + messages.Add(new ChatMessage + { + Role = "user", + MetaKind = item.RequestInterrupt ? "queued_prompt_interrupt" : "queued_prompt", + Content = item.Content, + Timestamp = item.CreatedAt, + }); + events.Add(new AgentQueuedCommandProjectionEvent(AgentEventType.UserMessage, "", item.Content)); + break; + } + } + + if (deferredCount > 0) + { + events.Add(new AgentQueuedCommandProjectionEvent( + AgentEventType.Thinking, + "", + $"Deferred {deferredCount} lower-priority queued item(s) for a later turn.")); + } + + return new AgentQueuedCommandProjectionResult(messages, events); + } +} diff --git a/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs b/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs index 51fa52a..9cb8658 100644 --- a/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs +++ b/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs @@ -240,6 +240,8 @@ public static class ArtifactQualityReviewService issues.Add(new("Summary sheet could better surface KPIs, decisions, or highlights.", ArtifactReviewSeverity.Info)); if (input.HasSummarySheet && input.DetailSheetCount >= 2 && !input.HasDashboardSheet) issues.Add(new("Workbook could benefit from a dashboard sheet to summarize multi-sheet trends.", ArtifactReviewSeverity.Info)); + if (input.HasDashboardSheet && !input.HasScorecardSection && !input.HasDecisionSection) + issues.Add(new("Dashboard sheet lacks KPI, trend, or decision content.", ArtifactReviewSeverity.Info)); if (input.HasDashboardSheet && !input.HasHighlightSection && !input.HasActionSection) issues.Add(new("Dashboard sheet could better call out highlights or actions.", ArtifactReviewSeverity.Info)); if (input.HasDashboardSheet && input.DetailSheetCount >= 2 && input.HyperlinkCount == 0) diff --git a/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs b/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs index 85f248f..b41d781 100644 --- a/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs +++ b/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs @@ -53,6 +53,8 @@ public static class ArtifactRepairGuideService private static string? BuildWorkbookAction(string message) { + if (message.Contains("Dashboard sheet lacks KPI, trend, or decision content", StringComparison.OrdinalIgnoreCase)) + return "Add KPI, trend, or decision blocks so the dashboard communicates the core story at a glance"; if (message.Contains("dashboard sheet", StringComparison.OrdinalIgnoreCase)) return "Add a dashboard sheet with KPI tiles, trends, and links to detail sheets"; if (message.Contains("Summary sheet could better surface", StringComparison.OrdinalIgnoreCase)) diff --git a/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs b/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs index 046a57f..1443af5 100644 --- a/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs +++ b/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs @@ -157,6 +157,12 @@ public static class DeckQualityReviewService issues.Add(new("Too many slides are text-heavy.", DeckReviewSeverity.Warning)); if (chartSlides + tableSlides + comparisonSlides == 0 && slideCount >= 4) issues.Add(new("Evidence slides such as charts, tables, or comparisons are limited.", DeckReviewSeverity.Warning)); + if (StorylineExpects(storylineSteps, "option", "options", "comparison", "alternatives") && comparisonSlides == 0) + issues.Add(new("Storyline expects an options or comparison slide but none is present.", DeckReviewSeverity.Warning)); + if (StorylineExpects(storylineSteps, "roadmap", "plan", "execution") && roadmapSlides == 0) + issues.Add(new("Storyline expects a roadmap slide but none is present.", DeckReviewSeverity.Warning)); + if (StorylineExpects(storylineSteps, "appendix", "evidence", "reference", "부록", "참고") && appendixSlides == 0) + issues.Add(new("Storyline expects appendix or evidence support but none is present.", DeckReviewSeverity.Info)); if (placeholderCount > 0) issues.Add(new($"Found {placeholderCount} placeholder or unfinished marker(s).", DeckReviewSeverity.Critical)); var score = 70; @@ -300,5 +306,19 @@ public static class DeckQualityReviewService return patterns.Sum(pattern => Regex.Matches(text, pattern, RegexOptions.IgnoreCase).Count); } + + private static bool StorylineExpects(IEnumerable storylineSteps, params string[] keywords) + { + foreach (var step in storylineSteps) + { + foreach (var keyword in keywords) + { + if (step.Contains(keyword, StringComparison.OrdinalIgnoreCase)) + return true; + } + } + + return false; + } } diff --git a/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs b/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs index 340919c..e1621f0 100644 --- a/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs +++ b/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs @@ -26,6 +26,12 @@ public static class DeckRepairGuideService => "Tighten slide headlines to one clear message sentence", var message when message.Contains("content density is high", StringComparison.OrdinalIgnoreCase) => "Reduce text density and convert bullets into cards, visuals, or sharper evidence points", + var message when message.Contains("Storyline expects an options or comparison slide", StringComparison.OrdinalIgnoreCase) + => "Add an options or comparison slide so the deck matches the intended storyline", + var message when message.Contains("Storyline expects a roadmap slide", StringComparison.OrdinalIgnoreCase) + => "Add a roadmap slide that turns the storyline into a delivery sequence", + var message when message.Contains("Storyline expects appendix or evidence support", StringComparison.OrdinalIgnoreCase) + => "Add appendix or evidence support slides to back the main storyline", var message when message.Contains("comparison slide", StringComparison.OrdinalIgnoreCase) => "Expand the comparison slide to show at least two real options and a verdict", var message when message.Contains("chart slide", StringComparison.OrdinalIgnoreCase)