???? ?? ? ??? ???? workbook/deck ?? ??? ??

?? ??
- AgentLoopService? ?? ?? ? ?? ??? ?? ??? ??? ?? ??? ? ??? ??? ??? ??
- XLSX dashboard workbook? PPT deck? ?? ?? ??? ??? ?? dashboard, storyline ?? ????? ? ? ????? ??

?? ????
- AgentQueuedCommandProjector? ??? queued command ??? queued_input_interrupt, queue_notification, queue_resume, queued_prompt ?? ???? thinking/user ???? ???? ?? helper? ??
- AgentLoopService? drain? ? ??? ?? switch? ?? ?? projector ??? ???? ??? ???
- ArtifactQualityReviewService? dashboard sheet? KPI, trend, decision ?? ?? ??? ???? workbook review ??? ??
- ArtifactRepairGuideService? ? dashboard ??? core story ?? ?? ???? ????? ??
- DeckQualityReviewService? storyline? Options, Roadmap, Appendix? ????? ?? ????? ?? ? ?? ?? ??? ????? ??
- DeckRepairGuideService? storyline ?? ??? deck storyline ?? ???? ????? ??
- AgentQueuedCommandProjectorTests, DeckQualityReviewServiceTests, ArtifactQualityReviewServiceTests, ArtifactRepairGuideServiceTests, DeckRepairGuideServiceTests? ??? ??? ??
- README.md? docs/DEVELOPMENT.md? 2026-04-15 09:24 (KST) ?? ??? ?? ??? ??

?? ??
- 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
This commit is contained in:
2026-04-15 08:58:19 +09:00
parent 918d62b8d5
commit ff29a83039
13 changed files with 281 additions and 84 deletions

View File

@@ -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);
}
/// <summary>에이전트 이벤트 스트림 (UI 바인딩용).</summary>

View File

@@ -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<ChatMessage> Messages,
IReadOnlyList<AgentQueuedCommandProjectionEvent> Events);
/// <summary>
/// 큐에서 꺼낸 명령 배치를 실제 대화 메시지와 이벤트로 투영합니다.
/// AgentLoopService 내부 분기 부담을 줄이고 테스트 가능한 형태로 분리합니다.
/// </summary>
internal static class AgentQueuedCommandProjector
{
public static AgentQueuedCommandProjectionResult Project(
IReadOnlyList<AgentQueuedCommand> drained,
int deferredCount)
{
var messages = new List<ChatMessage>();
var events = new List<AgentQueuedCommandProjectionEvent>();
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);
}
}

View File

@@ -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)

View File

@@ -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))

View File

@@ -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<string> storylineSteps, params string[] keywords)
{
foreach (var step in storylineSteps)
{
foreach (var keyword in keywords)
{
if (step.Contains(keyword, StringComparison.OrdinalIgnoreCase))
return true;
}
}
return false;
}
}

View File

@@ -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)