From f8067a1f9b6ad4cc02660c2589ce5adfd935a65c Mon Sep 17 00:00:00 2001 From: lacvet Date: Wed, 15 Apr 2026 21:49:09 +0900 Subject: [PATCH] =?UTF-8?q?AX=20Agent=20=EC=B0=BD=20=EC=A0=9C=EB=AA=A9=20?= =?UTF-8?q?=EA=B3=A0=EC=A0=95=20=EB=B0=8F=20PPT=20=ED=92=88=EC=A7=88=20?= =?UTF-8?q?=EC=9E=AC=EB=A3=A8=ED=94=84=20=EA=B3=A0=EB=8F=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 채팅창 Windows 제목을 AX Agent로 고정해 작업 표시줄/Alt+Tab에서 대화 제목이 섞이지 않도록 조정 - Assets/ppt manifest와 템플릿 카탈로그를 추가해 basic100/core100 등 고품질 PPT 자산을 실행 폴더, 소스 루트, AppData 템플릿 폴더에서 안정적으로 탐색하도록 개선 - pptx_create 결과에서 asset missing과 color fallback을 구분해 진단 메시지를 남기고 Cowork에서는 PPT quality/slide alerts/Needs work 기준으로 최대 2회 재생성 루프를 타도록 품질 게이트 추가 - PPT 시스템 프롬프트와 pptx-creator 스킬 지시를 document_plan -> pptx_create 중심으로 정렬 - 검증: dotnet build ...verify_ppt_quality_gate 경고 0/오류 0, dotnet test ...PptxSkillTemplatePackTests|PptxSkillAutoRepairTests|PptxSkillGoldenDeckTests|PptQualityGatePolicyTests|PptxTemplateManifestCatalogTests 통과 12 --- README.md | 10 + docs/DEVELOPMENT.md | 14 + .../Services/PptQualityGatePolicyTests.cs | 56 ++++ .../Services/PptxSkillTemplatePackTests.cs | 1 + .../PptxTemplateManifestCatalogTests.cs | 43 +++ .../Assets/ppt/templates.manifest.json | 76 +++++ src/AxCopilot/AxCopilot.csproj | 3 + .../Services/Agent/AgentLoopService.cs | 3 +- .../Agent/AgentLoopTransitions.Documents.cs | 53 +++- .../Agent/AgentLoopTransitions.Execution.cs | 1 + .../Services/Agent/PptQualityGatePolicy.cs | 92 ++++++ src/AxCopilot/Services/Agent/PptxSkill.cs | 108 +++----- .../Agent/PptxTemplateManifestCatalog.cs | 262 ++++++++++++++++++ .../Views/ChatWindow.SystemPromptBuilder.cs | 4 +- src/AxCopilot/Views/ChatWindow.xaml | 2 +- src/AxCopilot/skills/pptx-creator.skill.md | 6 +- 16 files changed, 666 insertions(+), 68 deletions(-) create mode 100644 src/AxCopilot.Tests/Services/PptQualityGatePolicyTests.cs create mode 100644 src/AxCopilot.Tests/Services/PptxTemplateManifestCatalogTests.cs create mode 100644 src/AxCopilot/Assets/ppt/templates.manifest.json create mode 100644 src/AxCopilot/Services/Agent/PptQualityGatePolicy.cs create mode 100644 src/AxCopilot/Services/Agent/PptxTemplateManifestCatalog.cs diff --git a/README.md b/README.md index b96920f..9a3c43c 100644 --- a/README.md +++ b/README.md @@ -2269,3 +2269,13 @@ MIT License 업데이트: 2026-04-15 21:23 (KST) - AX Agent 라이브 진행 문구와 시간·토큰 표시가 서로 분리돼 보이던 입력 영역 레이아웃을 다시 다듬었습니다. src/AxCopilot/Views/ChatWindow.xaml에서 StreamMetricsLabel을 독립 줄로 두지 않고 라이브 진행 Grid의 우측 하단에 다시 배치해, 진행 문구와 입력창이 한 덩어리처럼 붙어 보이면서도 시간·토큰 표시는 입력창 바로 위 우측에 유지되도록 조정했습니다. - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_stream_metrics_compact\\ -p:IntermediateOutputPath=obj\\verify_stream_metrics_compact\\ 경고 0 / 오류 0 + +업데이트: 2026-04-15 21:48 (KST) +- AX Agent 채팅창의 Windows 제목을 대화명과 분리해 `AX Agent`로 고정했습니다. `src/AxCopilot/Views/ChatWindow.xaml`의 창 제목이 더 이상 현재 대화 제목과 섞여 `AX Copilot — ...` 형태로 보이지 않습니다. +- PPT 템플릿 해상도 경로를 `Assets/ppt` manifest 기반으로 다시 정리했습니다. `src/AxCopilot/Assets/ppt/templates.manifest.json`, `src/AxCopilot/Services/Agent/PptxTemplateManifestCatalog.cs`, `src/AxCopilot/Services/Agent/PptxSkill.cs`가 이제 템플릿 키/별칭/기본 테마를 manifest에서 읽고, 실행 폴더·소스 루트·%APPDATA% 템플릿 폴더를 상향 탐색해 실제 `.pptx`를 찾습니다. 기존 개발 출력 경로에서 `src/AxCopilot/Assets/ppt`를 제대로 못 찾아 `color fallback`으로 빠지던 원인을 줄였습니다. +- PPT 생성 결과도 `asset missing`과 `color fallback`을 구분해 표시하도록 바꿨습니다. 템플릿 파일 자체를 못 찾은 경우에는 `Template diagnostics` 라인을 남겨 경로 문제와 마스터 복제 실패를 바로 구분할 수 있습니다. +- Cowork의 `pptx_create`는 이제 성공 직후 바로 종료되지 않고, `src/AxCopilot/Services/Agent/PptQualityGatePolicy.cs`와 `src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs`에서 품질 점수/슬라이드 경고/Needs work를 읽어 기준 미만이면 최대 2회까지 재생성을 요구합니다. 저품질 덱이 한 번 생성됐다는 이유만으로 루프가 끝나던 흐름을 막았습니다. +- `src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs`와 `src/AxCopilot/skills/pptx-creator.skill.md`도 PPT는 품질이 중요한 업무형 덱일수록 `document_plan -> pptx_create`를 기본 경로로 쓰고, 낮은 품질 요약이 나오면 바로 종료하지 말고 재생성하라고 맞췄습니다. +- 검증: + - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_quality_gate\\ -p:IntermediateOutputPath=obj\\verify_ppt_quality_gate\\` 경고 0 / 오류 0 + - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "PptxSkillTemplatePackTests|PptxSkillAutoRepairTests|PptxSkillGoldenDeckTests|PptQualityGatePolicyTests|PptxTemplateManifestCatalogTests" -p:OutputPath=bin\\verify_ppt_quality_gate_tests\\ -p:IntermediateOutputPath=obj\\verify_ppt_quality_gate_tests\\` 통과 12 diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 0de9d2b..84a5cfa 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1588,3 +1588,17 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫 - AX Agent 입력 영역 상단 레이아웃을 다시 정리했습니다. src/AxCopilot/Views/ChatWindow.xaml에서 StreamMetricsLabel을 독립 줄로 분리했던 배치 때문에 라이브 진행 문구와 입력창 사이가 넓게 벌어져 보였고, 우측 시간·토큰 표시만 아래로 내려간 상태였습니다. - 같은 파일의 라이브 진행 Grid를 2열 구조로 바꾸고 StreamMetricsLabel을 우측 하단에 붙여, 진행 문구는 입력창과 시각적으로 붙은 상태를 유지하면서도 시간·토큰은 항상 입력창 바로 위 우측에 고정되도록 조정했습니다. - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_stream_metrics_compact\\ -p:IntermediateOutputPath=obj\\verify_stream_metrics_compact\\ 경고 0 / 오류 0 + +업데이트: 2026-04-15 21:48 (KST) +- AX Agent 채팅창의 Windows 제목을 `AX Agent`로 고정했습니다. `src/AxCopilot/Views/ChatWindow.xaml`이 더 이상 대화 제목을 창 제목에 섞어 보여주지 않아, 작업 표시줄/Alt+Tab/미리보기에서 제품명만 안정적으로 보입니다. +- PPT 템플릿 해상도 경로를 manifest 기반으로 재구성했습니다. `src/AxCopilot/Assets/ppt/templates.manifest.json`에 템플릿 키, 파일명, fallback theme, 태그/pack hint를 선언하고 `src/AxCopilot/Services/Agent/PptxTemplateManifestCatalog.cs`가 실행 폴더, 현재 작업 폴더, `%APPDATA%\\AXCopilot\\templates\\ppt`, 소스 루트의 `src/AxCopilot/Assets/ppt`를 상향 탐색해 실제 템플릿 파일을 찾도록 바꿨습니다. 이전 `ResolveTemplatePath(...)`는 `bin\\...\\Assets\\ppt`처럼 잘못된 상대 경로에 의존해 개발 빌드 출력에서 `basic100/core100`을 놓칠 수 있었습니다. +- `src/AxCopilot/Services/Agent/PptxSkill.cs`는 manifest 해상도 결과를 받아 명시적 템플릿/pack 템플릿의 fallback theme를 더 정확히 고르고, 결과 요약에서 `color fallback`과 `asset missing -> built-in fallback`을 구분해 남깁니다. 템플릿 파일을 못 찾은 경우에는 `Template diagnostics` 라인에 후보 디렉터리 수를 함께 남겨 원인 추적이 쉬워졌습니다. +- Cowork PPT 품질 게이트를 추가했습니다. `src/AxCopilot/Services/Agent/PptQualityGatePolicy.cs`가 `pptx_create` 출력에서 `PPT quality`, `Slide alerts`, `Needs work`, `color fallback`을 파싱하고, `src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs`는 Cowork에서 `pptx_create` 성공 직후 품질이 기준 미만이면 즉시 종료하지 않고 최대 2회까지 `document_plan`/`pptx_create` 재생성을 요구합니다. 그동안 `AgentLoopService`의 terminal document completion 때문에 저품질 PPT도 첫 성공 후 바로 끝나던 문제가 있었습니다. +- Cowork 시스템 프롬프트와 PPT 스킬 지시도 품질 기준으로 정렬했습니다. `src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs`는 전략/제안/경영진용 PPT에서 `document_plan`을 먼저 쓰게 유도하고, `src/AxCopilot/skills/pptx-creator.skill.md`는 `document_plan + pptx_create`를 기본 경로로 명시하며 낮은 품질 요약이 나오면 바로 종료하지 말고 재생성하도록 안내합니다. +- 테스트: + - `src/AxCopilot.Tests/Services/PptQualityGatePolicyTests.cs` + - `src/AxCopilot.Tests/Services/PptxTemplateManifestCatalogTests.cs` + - `src/AxCopilot.Tests/Services/PptxSkillTemplatePackTests.cs` +- 검증: + - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_quality_gate\\ -p:IntermediateOutputPath=obj\\verify_ppt_quality_gate\\` 경고 0 / 오류 0 + - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "PptxSkillTemplatePackTests|PptxSkillAutoRepairTests|PptxSkillGoldenDeckTests|PptQualityGatePolicyTests|PptxTemplateManifestCatalogTests" -p:OutputPath=bin\\verify_ppt_quality_gate_tests\\ -p:IntermediateOutputPath=obj\\verify_ppt_quality_gate_tests\\` 통과 12 diff --git a/src/AxCopilot.Tests/Services/PptQualityGatePolicyTests.cs b/src/AxCopilot.Tests/Services/PptQualityGatePolicyTests.cs new file mode 100644 index 0000000..bc175bb --- /dev/null +++ b/src/AxCopilot.Tests/Services/PptQualityGatePolicyTests.cs @@ -0,0 +1,56 @@ +using AxCopilot.Services.Agent; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class PptQualityGatePolicyTests +{ + [Fact] + public void TryAssess_ShouldRequestRetry_ForLowScoreDeck() + { + var result = ToolResult.Ok( + "PPTX created: E:\\docu\\deck.pptx (10 slides, template:basic100 (color fallback), 16:9)\n" + + "PPT quality 64/100 | Strengths: Includes title slide | Slide alerts: 4 | Needs work: Too many slides are text-heavy."); + + var parsed = PptQualityGatePolicy.TryAssess(result, out var assessment); + + parsed.Should().BeTrue(); + assessment.Score.Should().Be(64); + assessment.SlideAlertCount.Should().Be(4); + assessment.HasColorFallback.Should().BeTrue(); + assessment.RequiresRetry.Should().BeTrue(); + } + + [Fact] + public void TryAssess_ShouldPass_ForCleanDeck() + { + var result = ToolResult.Ok( + "PPTX created: E:\\docu\\deck.pptx (6 slides, template:basic100 (master cloned), 16:9)\n" + + "PPT quality 88/100 | Strengths: Includes title slide | Needs work: none"); + + var parsed = PptQualityGatePolicy.TryAssess(result, out var assessment); + + parsed.Should().BeTrue(); + assessment.Score.Should().Be(88); + assessment.RequiresRetry.Should().BeFalse(); + } + + [Fact] + public void BuildRetryPrompt_ShouldRecommendDocumentPlan_WhenStorylineWasNotPrepared() + { + var assessment = new PptQualityGateAssessment( + 64, + 3, + HasColorFallback: false, + "Too many slides are text-heavy.", + RequiresRetry: true, + Summary: "점수 64/100 (기준 80), 슬라이드 경고 3건"); + + var prompt = PptQualityGatePolicy.BuildRetryPrompt(assessment, "E:\\docu\\deck.pptx", documentPlanWasCalled: false); + + prompt.Should().Contain("document_plan"); + prompt.Should().Contain("pptx_create"); + prompt.Should().Contain("E:\\docu\\deck.pptx"); + } +} diff --git a/src/AxCopilot.Tests/Services/PptxSkillTemplatePackTests.cs b/src/AxCopilot.Tests/Services/PptxSkillTemplatePackTests.cs index a719d62..0ef5c22 100644 --- a/src/AxCopilot.Tests/Services/PptxSkillTemplatePackTests.cs +++ b/src/AxCopilot.Tests/Services/PptxSkillTemplatePackTests.cs @@ -49,6 +49,7 @@ public class PptxSkillTemplatePackTests result.Success.Should().BeTrue(); result.Output.Should().Contain("Template pack: board"); + result.Output.Should().NotContain("asset missing"); File.Exists(Path.Combine(workDir, "board-pack-deck.pptx")).Should().BeTrue(); } finally diff --git a/src/AxCopilot.Tests/Services/PptxTemplateManifestCatalogTests.cs b/src/AxCopilot.Tests/Services/PptxTemplateManifestCatalogTests.cs new file mode 100644 index 0000000..db35885 --- /dev/null +++ b/src/AxCopilot.Tests/Services/PptxTemplateManifestCatalogTests.cs @@ -0,0 +1,43 @@ +using System.IO; +using AxCopilot.Services.Agent; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class PptxTemplateManifestCatalogTests +{ + [Fact] + public void Resolve_ShouldReturnManifestEntry_ForKnownTemplate() + { + var entry = PptxTemplateManifestCatalog.Resolve("basic100"); + + entry.Should().NotBeNull(); + entry!.FileName.Should().Be("BASIC100 기준 템플릿 V1.pptx"); + entry.FallbackTheme.Should().Be("professional"); + } + + [Fact] + public void ResolveTemplate_ShouldFindBundledSourceAsset_FromTestRuntime() + { + var resolution = PptxTemplateManifestCatalog.ResolveTemplate("basic100"); + + resolution.Entry.Should().NotBeNull(); + resolution.IsResolved.Should().BeTrue(); + resolution.Status.Should().Be("resolved"); + File.Exists(resolution.ResolvedPath).Should().BeTrue(); + Path.GetFileName(resolution.ResolvedPath!).Should().Be(resolution.Entry!.FileName); + } + + [Fact] + public void EnumerateCandidateDirectories_ShouldIncludeProjectSourceAssets() + { + var directories = PptxTemplateManifestCatalog.EnumerateCandidateDirectoriesForTesting( + AppContext.BaseDirectory, + Environment.CurrentDirectory, + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); + + directories.Should().Contain(path => + path.EndsWith(Path.Combine("src", "AxCopilot", "Assets", "ppt"), StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/src/AxCopilot/Assets/ppt/templates.manifest.json b/src/AxCopilot/Assets/ppt/templates.manifest.json new file mode 100644 index 0000000..2b2321a --- /dev/null +++ b/src/AxCopilot/Assets/ppt/templates.manifest.json @@ -0,0 +1,76 @@ +{ + "templates": [ + { + "key": "basic100", + "fileName": "BASIC100 기준 템플릿 V1.pptx", + "displayName": "BASIC100 기준 템플릿 V1", + "fallbackTheme": "professional", + "aliases": ["basic", "consulting_basic"], + "tags": ["executive", "strategy", "consulting", "brand"], + "packHints": ["strategy", "pmo", "sales", "operating_model"] + }, + { + "key": "core100", + "fileName": "CORE100 기준템플릿 V1.pptx", + "displayName": "CORE100 기준 템플릿 V1", + "fallbackTheme": "corporate", + "aliases": ["core", "board_core"], + "tags": ["board", "governance", "finance", "corporate"], + "packHints": ["board", "finance"] + }, + { + "key": "frame_blue", + "fileName": "P01_01_프레임디자인_PPT템플릿_블루(PPT_편집용).pptx", + "displayName": "프레임디자인 블루", + "fallbackTheme": "modern", + "aliases": ["blue_frame"], + "tags": ["frame", "blue", "report", "proposal"], + "packHints": ["sales", "operating_model"] + }, + { + "key": "mr_ppt_01", + "fileName": "미스터 피피티 템플릿 01_원본.pptx", + "displayName": "미스터 피피티 템플릿 01", + "fallbackTheme": "modern", + "aliases": ["mrppt01"], + "tags": ["creative", "proposal", "sales"], + "packHints": ["sales"] + }, + { + "key": "mr_ppt_02", + "fileName": "미스터 피피티 템플릿 02_원본.pptx", + "displayName": "미스터 피피티 템플릿 02", + "fallbackTheme": "vibrant", + "aliases": ["mrppt02"], + "tags": ["creative", "marketing", "proposal"], + "packHints": ["sales"] + }, + { + "key": "mr_ppt_03", + "fileName": "미스터 피피티 03_원본.pptx", + "displayName": "미스터 피피티 템플릿 03", + "fallbackTheme": "minimal", + "aliases": ["mrppt03"], + "tags": ["minimal", "analysis", "report"], + "packHints": ["strategy", "finance"] + }, + { + "key": "mr_ppt_04", + "fileName": "미스터 피피티 템플릿 04_원본.pptx", + "displayName": "미스터 피피티 템플릿 04", + "fallbackTheme": "slate", + "aliases": ["mrppt04"], + "tags": ["dark", "executive", "review"], + "packHints": ["board", "finance"] + }, + { + "key": "mr_ppt_05", + "fileName": "미스터 피피티 템플릿_05_원본.pptx", + "displayName": "미스터 피피티 템플릿 05", + "fallbackTheme": "ocean", + "aliases": ["mrppt05"], + "tags": ["blue", "modern", "report"], + "packHints": ["pmo", "operating_model"] + } + ] +} diff --git a/src/AxCopilot/AxCopilot.csproj b/src/AxCopilot/AxCopilot.csproj index aae989a..965c43f 100644 --- a/src/AxCopilot/AxCopilot.csproj +++ b/src/AxCopilot/AxCopilot.csproj @@ -121,6 +121,9 @@ + + PreserveNewest + diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs index eae8064..80ae5b8 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopService.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs @@ -1756,7 +1756,8 @@ public partial class AgentLoopService executionPolicy, context, ct, - documentPlanCalled); + documentPlanCalled, + runState); if (terminalCompleted) { if (consumedExtraIteration) diff --git a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs index 7662bdd..a89a431 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs @@ -94,11 +94,15 @@ public partial class AgentLoopService ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy, AgentContext context, CancellationToken ct, - bool documentPlanWasCalled = false) + bool documentPlanWasCalled = false, + RunState? runState = null) { if (!result.Success || !IsTerminalDocumentTool(call.ToolName) || toolCalls.Count != 1) return (false, false); + if (TryApplyPptQualityGateTransition(call, result, messages, documentPlanWasCalled, runState)) + return (false, false); + if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) { EmitEvent(AgentEventType.Complete, "", "에이전트 작업 완료"); @@ -124,6 +128,53 @@ public partial class AgentLoopService return (true, consumedExtraIteration); } + private bool TryApplyPptQualityGateTransition( + ContentBlock call, + ToolResult result, + List messages, + bool documentPlanWasCalled, + RunState? runState) + { + if (!string.Equals(ActiveTab, "Cowork", StringComparison.OrdinalIgnoreCase)) + return false; + + if (!string.Equals(call.ToolName, "pptx_create", StringComparison.OrdinalIgnoreCase)) + return false; + + if (!PptQualityGatePolicy.TryAssess(result, out var assessment)) + return false; + + if (!assessment.RequiresRetry) + return false; + + if (runState == null) + return false; + + if (runState.PptQualityGateRetry >= PptQualityGatePolicy.DefaultMaxRetries) + { + EmitEvent( + AgentEventType.Thinking, + "", + $"PPT 품질 재생성 한도에 도달해 현재 결과로 종료합니다 ({assessment.Summary})"); + return false; + } + + runState.PptQualityGateRetry++; + messages.Add(new ChatMessage + { + Role = "user", + Content = PptQualityGatePolicy.BuildRetryPrompt( + assessment, + result.FilePath, + documentPlanWasCalled) + }); + EmitEvent( + AgentEventType.Thinking, + "", + $"PPT 품질이 기준 미만이라 재생성을 요청합니다 ({runState.PptQualityGateRetry}/{PptQualityGatePolicy.DefaultMaxRetries})"); + return true; + } + private async Task TryApplyPostToolVerificationTransitionAsync( ContentBlock call, ToolResult result, diff --git a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs index 0a8df55..e39a6a1 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs @@ -1242,6 +1242,7 @@ public partial class AgentLoopService public int TransientLlmErrorRetries; public int DocumentArtifactGateRetry; public int DocumentVerificationGateRetry; + public int PptQualityGateRetry; public int NoProgressRecoveryRetry; public int TerminalEvidenceGateRetry; public bool WorkspaceAppearsEmpty; diff --git a/src/AxCopilot/Services/Agent/PptQualityGatePolicy.cs b/src/AxCopilot/Services/Agent/PptQualityGatePolicy.cs new file mode 100644 index 0000000..a92db90 --- /dev/null +++ b/src/AxCopilot/Services/Agent/PptQualityGatePolicy.cs @@ -0,0 +1,92 @@ +using System.Text.RegularExpressions; + +namespace AxCopilot.Services.Agent; + +public sealed record PptQualityGateAssessment( + int Score, + int SlideAlertCount, + bool HasColorFallback, + string NeedsWork, + bool RequiresRetry, + string Summary); + +public static class PptQualityGatePolicy +{ + public const int DefaultMinScore = 80; + public const int DefaultMaxRetries = 2; + + public static bool TryAssess(ToolResult result, out PptQualityGateAssessment assessment, int minScore = DefaultMinScore) + { + assessment = new PptQualityGateAssessment(0, 0, false, "", false, ""); + if (!result.Success || string.IsNullOrWhiteSpace(result.Output)) + return false; + + var output = result.Output; + var scoreMatch = Regex.Match(output, @"PPT quality\s+(?\d{1,3})/100", RegexOptions.IgnoreCase); + if (!scoreMatch.Success || !int.TryParse(scoreMatch.Groups["score"].Value, out var score)) + return false; + + var alertMatch = Regex.Match(output, @"Slide alerts:\s*(?\d+)", RegexOptions.IgnoreCase); + var needsWorkMatch = Regex.Match(output, @"Needs work:\s*(?.+?)(?:\||\r?\n|$)", RegexOptions.IgnoreCase); + var slideAlerts = alertMatch.Success && int.TryParse(alertMatch.Groups["count"].Value, out var parsedAlerts) + ? parsedAlerts + : 0; + var needsWork = needsWorkMatch.Success ? needsWorkMatch.Groups["value"].Value.Trim() : ""; + var hasColorFallback = output.Contains("color fallback", StringComparison.OrdinalIgnoreCase); + var requiresRetry = score < minScore + || slideAlerts > 0 + || (!string.IsNullOrWhiteSpace(needsWork) + && !string.Equals(needsWork, "none", StringComparison.OrdinalIgnoreCase)); + var summary = BuildSummary(score, slideAlerts, hasColorFallback, needsWork, minScore); + + assessment = new PptQualityGateAssessment( + score, + slideAlerts, + hasColorFallback, + needsWork, + requiresRetry, + summary); + return true; + } + + public static string BuildRetryPrompt( + PptQualityGateAssessment assessment, + string? filePath, + bool documentPlanWasCalled) + { + var target = string.IsNullOrWhiteSpace(filePath) ? "방금 생성한 PPT 파일" : $"'{filePath}'"; + var planStep = documentPlanWasCalled + ? "기존 스토리라인을 더 날카롭게 다듬고 같은 파일 경로로 다시 생성하세요." + : "먼저 document_plan으로 스토리라인을 재정리한 뒤, 같은 파일 경로로 pptx_create를 다시 호출하세요."; + var templateStep = assessment.HasColorFallback + ? "템플릿 마스터 복제가 끝까지 적용되지 않았으니 template/template_pack 선택을 다시 점검하고 브랜드 템플릿을 우선 사용하세요." + : "template_pack(strategy/board/pmo/finance/sales/operating_model) 또는 적절한 브랜드 템플릿을 적극 사용하세요."; + var needsWorkLine = string.IsNullOrWhiteSpace(assessment.NeedsWork) + ? "- 주요 보완: 슬라이드당 메시지를 하나로 줄이고, 근거 슬라이드와 결론 슬라이드의 대비를 분명히 하세요.\n" + : $"- 주요 보완: {assessment.NeedsWork}\n"; + + return "[System:PptQualityGate] 방금 생성한 PPT 품질이 기준 미만입니다.\n" + + $"- 대상 파일: {target}\n" + + $"- 품질 요약: {assessment.Summary}\n" + + needsWorkLine + + "- 필수 개선 방향:\n" + + " 1. 한 슬라이드 한 메시지 원칙으로 텍스트 밀도를 낮추세요.\n" + + " 2. Executive Summary → Options/Comparison → Recommendation → Roadmap → Appendix/Evidence 흐름을 분명히 하세요.\n" + + " 3. 표/차트/비교 슬라이드로 정량 근거를 늘리고, 헤드라인을 더 결론형 문장으로 바꾸세요.\n" + + $" 4. {templateStep}\n" + + $" 5. {planStep}\n" + + "개선된 버전을 만든 뒤 다시 품질 요약이 좋아졌는지 확인하세요."; + } + + private static string BuildSummary(int score, int slideAlerts, bool hasColorFallback, string needsWork, int minScore) + { + var parts = new List { $"점수 {score}/100 (기준 {minScore})" }; + if (slideAlerts > 0) + parts.Add($"슬라이드 경고 {slideAlerts}건"); + if (hasColorFallback) + parts.Add("template color fallback 발생"); + if (!string.IsNullOrWhiteSpace(needsWork) && !string.Equals(needsWork, "none", StringComparison.OrdinalIgnoreCase)) + parts.Add($"Needs work: {needsWork}"); + return string.Join(", ", parts); + } +} diff --git a/src/AxCopilot/Services/Agent/PptxSkill.cs b/src/AxCopilot/Services/Agent/PptxSkill.cs index ac7746f..178f881 100644 --- a/src/AxCopilot/Services/Agent/PptxSkill.cs +++ b/src/AxCopilot/Services/Agent/PptxSkill.cs @@ -220,58 +220,21 @@ public class PptxSkill : IAgentTool // ── 통합 테마 레코드 ────────────────────────────────────────────────────── private record FullTheme(ThemeColors Colors, ThemeLayout Layout); - // ── 고품질 템플릿 레지스트리 ──────────────────────────────────────────────── - // 이름 → Assets/ppt/ 아래 파일명 매핑. 실행 시 디스크에서 열어 마스터 복제. - // 토큰 컨텍스트 영향 0 — 바이너리 파일은 LLM에 전달되지 않음. - private static readonly Dictionary TemplateRegistry = new(StringComparer.OrdinalIgnoreCase) + private static FullTheme ResolveTemplateFallbackTheme( + string templateName, + PptxTemplateResolution? resolution, + FullTheme defaultTheme) { - ["basic100"] = "BASIC100 기준 템플릿 V1.pptx", - ["core100"] = "CORE100 기준템플릿 V1.pptx", - ["frame_blue"] = "P01_01_프레임디자인_PPT템플릿_블루(PPT_편집용).pptx", - ["mr_ppt_01"] = "미스터 피피티 템플릿 01_원본.pptx", - ["mr_ppt_02"] = "미스터 피피티 템플릿 02_원본.pptx", - ["mr_ppt_03"] = "미스터 피피티 03_원본.pptx", - ["mr_ppt_04"] = "미스터 피피티 템플릿 04_원본.pptx", - ["mr_ppt_05"] = "미스터 피피티 템플릿_05_원본.pptx", - }; + if (FullThemes.TryGetValue(templateName, out var namedTheme)) + return namedTheme; - /// - /// 템플릿 이름으로 .pptx 파일 전체 경로를 반환합니다. - /// 설치파일 용량 문제로 템플릿은 빌드에 포함하지 않으며, - /// 여러 알려진 경로를 순서대로 탐색합니다. - /// - private static string? ResolveTemplatePath(string templateName) - { - if (!TemplateRegistry.TryGetValue(templateName, out var fileName)) - return null; - - var candidates = new List(); - var exeDir = AppContext.BaseDirectory; - - // 1) 실행 파일 옆 Assets/ppt/ (수동 배치) - candidates.Add(Path.Combine(exeDir, "Assets", "ppt", fileName)); - - // 2) %APPDATA%/AXCopilot/templates/ppt/ (사용자 설치) - var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - if (!string.IsNullOrEmpty(appData)) - candidates.Add(Path.Combine(appData, "AXCopilot", "templates", "ppt", fileName)); - - // 3) 소스 프로젝트 디렉토리 (개발 환경 — bin/Release/net8.0-windows... 기준) - candidates.Add(Path.GetFullPath(Path.Combine(exeDir, "..", "..", "..", "Assets", "ppt", fileName))); - - // 4) 워킹 디렉토리/src/AxCopilot/Assets/ppt/ (개발 환경 루트에서 실행) - candidates.Add(Path.Combine("src", "AxCopilot", "Assets", "ppt", fileName)); - - // 5) 프로젝트 루트 추정 (E:\AX Copilot - Claude 등) - candidates.Add(Path.GetFullPath(Path.Combine(exeDir, "..", "..", "..", "..", "..", "Assets", "ppt", fileName))); - - foreach (var path in candidates) + if (resolution?.Entry != null && + FullThemes.TryGetValue(resolution.Entry.FallbackTheme, out var manifestTheme)) { - if (File.Exists(path)) - return Path.GetFullPath(path); + return manifestTheme; } - return null; + return defaultTheme; } // ── 20가지 테마 사전 ────────────────────────────────────────────────────── @@ -588,6 +551,8 @@ public class PptxSkill : IAgentTool : null); var templatePackName = templatePack?.Name; var packTemplateName = templatePack?.PreferredTemplate; + PptxTemplateResolution? explicitTemplateResolution = null; + PptxTemplateResolution? packTemplateResolution = null; if (renderDeck != null) { @@ -647,24 +612,24 @@ public class PptxSkill : IAgentTool if (!string.IsNullOrWhiteSpace(templateName)) { - // 고품질 템플릿: 마스터 복제 시도 → 실패 시 내장 메타데이터 폴백 - templatePptxPath = ResolveTemplatePath(templateName!); + explicitTemplateResolution = PptxTemplateManifestCatalog.ResolveTemplate(templateName!); + templatePptxPath = explicitTemplateResolution.ResolvedPath; + var templateFallbackTheme = ResolveTemplateFallbackTheme( + templateName!, + explicitTemplateResolution, + FullThemes["professional"]); - // 내장 테마에 템플릿 이름이 등록되어 있으면 해당 색상/레이아웃 사용 - if (FullThemes.TryGetValue(templateName!, out var templateTheme)) - fullTheme = templateTheme; - else if (templatePptxPath != null) + if (templatePptxPath != null) { - // 미등록 템플릿이지만 파일이 있으면 색상 추출 var extracted = ExtractThemeFromPptx(templatePptxPath); - var baseLayout = FullThemes["professional"].Layout; + var baseLayout = templateFallbackTheme.Layout; fullTheme = extracted != null ? new FullTheme(extracted, baseLayout) - : FullThemes["professional"]; + : templateFallbackTheme; } else { - fullTheme = FullThemes["professional"]; + fullTheme = templateFallbackTheme; } } else if (args.SafeTryGetProperty("theme_file", out var tfEl) && !string.IsNullOrEmpty(tfEl.SafeGetString())) @@ -683,7 +648,10 @@ public class PptxSkill : IAgentTool else if (templatePack != null) { if (!string.IsNullOrWhiteSpace(packTemplateName)) - templatePptxPath = ResolveTemplatePath(packTemplateName!); + { + packTemplateResolution = PptxTemplateManifestCatalog.ResolveTemplate(packTemplateName!); + templatePptxPath = packTemplateResolution.ResolvedPath; + } if (!FullThemes.TryGetValue(templatePack.FallbackTheme, out var packTheme)) packTheme = FullThemes["professional"]; @@ -1047,16 +1015,32 @@ public class PptxSkill : IAgentTool ? $"theme_file:{cloneInfo_srcLabel} (master cloned{(cloneAll ? " + slides cloned" : "")})" : theme : !string.IsNullOrWhiteSpace(templateName) - ? $"template:{templateName} (color fallback)" + ? templatePptxPath == null + ? $"template:{templateName} (asset missing -> built-in fallback)" + : $"template:{templateName} (color fallback)" : templatePptxPath != null && !string.IsNullOrWhiteSpace(packTemplateName) ? $"template:{packTemplateName} (color fallback)" - : templatePptxPath != null - ? $"theme_file:{cloneInfo_srcLabel} (master cloned{(cloneAll ? " + slides cloned" : "")})" - : theme; + : !string.IsNullOrWhiteSpace(packTemplateName) + ? $"template:{packTemplateName} (asset missing -> pack fallback)" + : templatePptxPath != null + ? $"theme_file:{cloneInfo_srcLabel} (color fallback)" + : theme; var outputParts = new List { $"PPTX created: {fullPath} ({slideCount_final} slides, {themeLabel}, {(isWide ? "16:9" : "4:3")})" }; + if (explicitTemplateResolution != null && explicitTemplateResolution.Status == "asset_missing") + { + outputParts.Add( + $"Template diagnostics: '{explicitTemplateResolution.RequestedName}' asset not found. " + + $"Searched {explicitTemplateResolution.CandidateDirectories.Count} candidate PPT directories."); + } + else if (packTemplateResolution != null && packTemplateResolution.Status == "asset_missing") + { + outputParts.Add( + $"Template diagnostics: pack template '{packTemplateResolution.RequestedName}' asset not found. " + + $"Searched {packTemplateResolution.CandidateDirectories.Count} candidate PPT directories."); + } if (renderDeck != null) outputParts.Add(renderDeck.ToToolSummary()); if (!string.IsNullOrWhiteSpace(templatePackName)) diff --git a/src/AxCopilot/Services/Agent/PptxTemplateManifestCatalog.cs b/src/AxCopilot/Services/Agent/PptxTemplateManifestCatalog.cs new file mode 100644 index 0000000..efef15a --- /dev/null +++ b/src/AxCopilot/Services/Agent/PptxTemplateManifestCatalog.cs @@ -0,0 +1,262 @@ +using System.IO; +using System.Text.Json; + +namespace AxCopilot.Services.Agent; + +public sealed record PptxTemplateManifestEntry( + string Key, + string FileName, + string DisplayName, + string FallbackTheme, + IReadOnlyList Aliases, + IReadOnlyList Tags, + IReadOnlyList PackHints); + +public sealed class PptxTemplateManifest +{ + public List Templates { get; init; } = new(); +} + +public sealed record PptxTemplateResolution( + string RequestedName, + PptxTemplateManifestEntry? Entry, + string? ResolvedPath, + string Status, + IReadOnlyList CandidateDirectories) +{ + public bool IsResolved => !string.IsNullOrWhiteSpace(ResolvedPath); +} + +public static class PptxTemplateManifestCatalog +{ + private const string ManifestFileName = "templates.manifest.json"; + private static readonly JsonSerializerOptions s_jsonOptions = new() + { + PropertyNameCaseInsensitive = true, + }; + + private static readonly object s_manifestLock = new(); + private static string? s_cachedManifestPath; + private static DateTime s_cachedManifestWriteTimeUtc; + private static PptxTemplateManifest? s_cachedManifest; + + public static IReadOnlyList GetAll() + => LoadManifest().Templates; + + public static PptxTemplateManifestEntry? Resolve(string? requestedName) + { + if (string.IsNullOrWhiteSpace(requestedName)) + return null; + + var normalized = requestedName.Trim(); + return LoadManifest().Templates.FirstOrDefault(entry => + string.Equals(entry.Key, normalized, StringComparison.OrdinalIgnoreCase) || + entry.Aliases.Any(alias => string.Equals(alias, normalized, StringComparison.OrdinalIgnoreCase))); + } + + public static PptxTemplateResolution ResolveTemplate(string? requestedName) + { + if (string.IsNullOrWhiteSpace(requestedName)) + return new PptxTemplateResolution("", null, null, "empty", Array.Empty()); + + var trimmed = requestedName.Trim(); + if (LooksLikePath(trimmed)) + { + var explicitPath = ResolveExplicitPath(trimmed); + return new PptxTemplateResolution( + trimmed, + null, + explicitPath, + explicitPath != null ? "explicit_path" : "explicit_path_missing", + explicitPath != null ? new[] { Path.GetDirectoryName(explicitPath)! } : Array.Empty()); + } + + var entry = Resolve(trimmed); + var candidateDirectories = EnumerateCandidateDirectories( + AppContext.BaseDirectory, + Environment.CurrentDirectory, + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); + + if (entry == null) + return new PptxTemplateResolution(trimmed, null, null, "unknown_template", candidateDirectories); + + foreach (var directory in candidateDirectories) + { + var path = Path.Combine(directory, entry.FileName); + if (!File.Exists(path)) + continue; + + return new PptxTemplateResolution( + trimmed, + entry, + Path.GetFullPath(path), + "resolved", + candidateDirectories); + } + + return new PptxTemplateResolution(trimmed, entry, null, "asset_missing", candidateDirectories); + } + + internal static IReadOnlyList EnumerateCandidateDirectoriesForTesting( + string baseDirectory, + string currentDirectory, + string? appDataPath) + => EnumerateCandidateDirectories(baseDirectory, currentDirectory, appDataPath); + + private static PptxTemplateManifest LoadManifest() + { + var manifestPath = FindManifestPath(); + if (string.IsNullOrWhiteSpace(manifestPath) || !File.Exists(manifestPath)) + return CreateFallbackManifest(); + + var fullPath = Path.GetFullPath(manifestPath); + var writeTimeUtc = File.GetLastWriteTimeUtc(fullPath); + + lock (s_manifestLock) + { + if (s_cachedManifest != null && + string.Equals(s_cachedManifestPath, fullPath, StringComparison.OrdinalIgnoreCase) && + s_cachedManifestWriteTimeUtc == writeTimeUtc) + { + return s_cachedManifest; + } + + try + { + var json = File.ReadAllText(fullPath); + var manifest = JsonSerializer.Deserialize(json, s_jsonOptions); + if (manifest?.Templates.Count > 0) + { + s_cachedManifestPath = fullPath; + s_cachedManifestWriteTimeUtc = writeTimeUtc; + s_cachedManifest = manifest; + return manifest; + } + } + catch + { + // fall through to the bundled fallback manifest + } + + s_cachedManifestPath = null; + s_cachedManifestWriteTimeUtc = DateTime.MinValue; + s_cachedManifest = CreateFallbackManifest(); + return s_cachedManifest; + } + } + + private static string? FindManifestPath() + { + var candidates = EnumerateCandidateDirectories( + AppContext.BaseDirectory, + Environment.CurrentDirectory, + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); + + foreach (var directory in candidates) + { + var manifestPath = Path.Combine(directory, ManifestFileName); + if (File.Exists(manifestPath)) + return Path.GetFullPath(manifestPath); + } + + return null; + } + + private static IReadOnlyList EnumerateCandidateDirectories( + string baseDirectory, + string currentDirectory, + string? appDataPath) + { + var directories = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + void Add(string? candidate) + { + if (string.IsNullOrWhiteSpace(candidate)) + return; + + string normalized; + try + { + normalized = Path.GetFullPath(candidate); + } + catch + { + return; + } + + if (seen.Add(normalized)) + directories.Add(normalized); + } + + if (!string.IsNullOrWhiteSpace(appDataPath)) + Add(Path.Combine(appDataPath, "AXCopilot", "templates", "ppt")); + + Add(Path.Combine(baseDirectory, "Assets", "ppt")); + Add(Path.Combine(currentDirectory, "Assets", "ppt")); + Add(Path.Combine(currentDirectory, "src", "AxCopilot", "Assets", "ppt")); + + foreach (var anchor in new[] { baseDirectory, currentDirectory }) + { + var current = SafeDirectoryInfo(anchor); + for (var depth = 0; current != null && depth < 10; depth++, current = current.Parent) + { + Add(Path.Combine(current.FullName, "Assets", "ppt")); + Add(Path.Combine(current.FullName, "src", "AxCopilot", "Assets", "ppt")); + } + } + + return directories; + } + + private static DirectoryInfo? SafeDirectoryInfo(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + return null; + + try + { + return new DirectoryInfo(Path.GetFullPath(path)); + } + catch + { + return null; + } + } + + private static bool LooksLikePath(string value) + => value.Contains('\\', StringComparison.Ordinal) + || value.Contains('/', StringComparison.Ordinal) + || Path.IsPathRooted(value); + + private static string? ResolveExplicitPath(string path) + { + try + { + var fullPath = Path.GetFullPath(path); + return File.Exists(fullPath) ? fullPath : null; + } + catch + { + return null; + } + } + + private static PptxTemplateManifest CreateFallbackManifest() + { + return new PptxTemplateManifest + { + Templates = + [ + new("basic100", "BASIC100 기준 템플릿 V1.pptx", "BASIC100 기준 템플릿 V1", "professional", ["basic", "consulting_basic"], ["executive", "strategy", "consulting", "brand"], ["strategy", "pmo", "sales", "operating_model"]), + new("core100", "CORE100 기준템플릿 V1.pptx", "CORE100 기준 템플릿 V1", "corporate", ["core", "board_core"], ["board", "governance", "finance", "corporate"], ["board", "finance"]), + new("frame_blue", "P01_01_프레임디자인_PPT템플릿_블루(PPT_편집용).pptx", "프레임디자인 블루", "modern", ["blue_frame"], ["frame", "blue", "report", "proposal"], ["sales", "operating_model"]), + new("mr_ppt_01", "미스터 피피티 템플릿 01_원본.pptx", "미스터 피피티 템플릿 01", "modern", ["mrppt01"], ["creative", "proposal", "sales"], ["sales"]), + new("mr_ppt_02", "미스터 피피티 템플릿 02_원본.pptx", "미스터 피피티 템플릿 02", "vibrant", ["mrppt02"], ["creative", "marketing", "proposal"], ["sales"]), + new("mr_ppt_03", "미스터 피피티 03_원본.pptx", "미스터 피피티 템플릿 03", "minimal", ["mrppt03"], ["minimal", "analysis", "report"], ["strategy", "finance"]), + new("mr_ppt_04", "미스터 피피티 템플릿 04_원본.pptx", "미스터 피피티 템플릿 04", "slate", ["mrppt04"], ["dark", "executive", "review"], ["board", "finance"]), + new("mr_ppt_05", "미스터 피피티 템플릿_05_원본.pptx", "미스터 피피티 템플릿 05", "ocean", ["mrppt05"], ["blue", "modern", "report"], ["pmo", "operating_model"]), + ] + }; + } +} diff --git a/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs b/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs index 10a742d..6797dfc 100644 --- a/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs +++ b/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs @@ -68,7 +68,8 @@ public partial class ChatWindow sb.AppendLine("For ordinary Cowork requests where no plan is requested, proceed directly with the work and focus on producing the requested result."); sb.AppendLine("If the user asks for a brand-new report, proposal, analysis, manual, or other document and does not explicitly ask to reference workspace files, do NOT start with glob, grep, document_read, or folder_map."); sb.AppendLine("In that case, go straight to the creation tool. Use document_plan only when multi-section structure work clearly improves the result or when the user explicitly requests a plan."); - sb.AppendLine("If the user asks for a presentation, slide deck, or PPT, prefer calling pptx_create directly. Use document_plan first only when the user explicitly asks for an outline or when the deck structure is genuinely unclear."); + sb.AppendLine("If the user asks for a presentation, slide deck, or PPT, prefer document_plan first for executive, business, strategy, review, proposal, or board-style decks, and especially when the deck is more than a few slides."); + sb.AppendLine("Use pptx_create directly only for clearly simple decks, explicit slide-by-slide edits, or when the user already provided a strong structure."); sb.AppendLine("When writing a new document, avoid repetitive same-shape sections. Tailor the structure to the purpose and use summaries, findings, comparison tables, timelines, recommendations, appendices, or action items when they improve clarity."); sb.AppendLine("Prefer concrete and useful content over filler. If a section benefits from bullets, tables, or structured comparison, use them instead of flat generic paragraphs."); sb.AppendLine("After creating files, summarize what was created and include the actual output path."); @@ -91,6 +92,7 @@ public partial class ChatWindow sb.AppendLine(" 4. Fill every section with real content. Do not stop at an outline or plan only."); sb.AppendLine(" 4-1. Use richer section patterns when they help: summary box, key findings bullets, comparison table, timeline, checklist, action items, appendix."); sb.AppendLine(" 5. The task is complete only after the actual document file has been created or updated."); + sb.AppendLine(" 6. For PPT work, if the quality summary mentions low score, slide alerts, or needs work, revise the storyline and regenerate instead of stopping after the first pptx_create call."); // 문서 품질 검증 루프 sb.AppendLine("\n## Document Quality Review"); diff --git a/src/AxCopilot/Views/ChatWindow.xaml b/src/AxCopilot/Views/ChatWindow.xaml index 8e6f08a..efbf84f 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml +++ b/src/AxCopilot/Views/ChatWindow.xaml @@ -5,7 +5,7 @@ xmlns:local="clr-namespace:AxCopilot.Views" xmlns:conv="clr-namespace:AxCopilot.Themes" xmlns:vm="clr-namespace:AxCopilot.ViewModels" - Title="AX Copilot — AX Agent" + Title="AX Agent" Width="1180" Height="880" MinWidth="780" MinHeight="560" WindowStyle="None" diff --git a/src/AxCopilot/skills/pptx-creator.skill.md b/src/AxCopilot/skills/pptx-creator.skill.md index b78b93b..8c3e803 100644 --- a/src/AxCopilot/skills/pptx-creator.skill.md +++ b/src/AxCopilot/skills/pptx-creator.skill.md @@ -14,14 +14,16 @@ allowed-tools: tabs: cowork --- -Use `pptx_create` as the default path. Do not start with ad-hoc slide arrays unless the user already gave a very detailed deck structure. +Use `document_plan` plus `pptx_create` as the default path for business decks. Do not start with ad-hoc slide arrays unless the user already gave a very detailed deck structure. ## Default workflow 1. Review the workspace for existing presentation files, reports, or templates. 2. Use `document_plan` with `document_type: presentation` to define the storyline first. 3. Build a deck brief with `audience`, `objective`, `decision_ask`, and optional `storyline`. 4. Generate the deck with `pptx_create`. -5. Return the file path together with the storyline and quality summary. +5. Prefer `template_pack` or a bundled template (`basic100`, `core100`, etc.) when the deck needs branded polish. +6. If the returned quality summary is weak, revise the storyline and regenerate the same file instead of stopping. +7. Return the file path together with the storyline and quality summary. ## Quality rules - Keep one message per slide.