에이전트 루프와 코드 언어 지원, 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

@@ -15,6 +15,14 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SkillServiceRuntimePolicyTests|SlashCommandCatalogTests|McpSkillCatalogTests" -p:OutputPath=bin\\verify_phase4_tests\\ -p:IntermediateOutputPath=obj\\verify_phase4_tests\\` 통과 17 - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SkillServiceRuntimePolicyTests|SlashCommandCatalogTests|McpSkillCatalogTests" -p:OutputPath=bin\\verify_phase4_tests\\ -p:IntermediateOutputPath=obj\\verify_phase4_tests\\` 통과 17
- 참고: 테스트 프로젝트의 기존 파일 `src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs(76)` nullable 경고 1건은 유지됩니다. - 참고: 테스트 프로젝트의 기존 파일 `src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs(76)` nullable 경고 1건은 유지됩니다.
- 업데이트: 2026-04-14 19:50 (KST)
- AX Agent의 내부 실행 품질을 `claude-code` 기준으로 한 단계 더 끌어올렸습니다. 실행 중 추가 입력은 [AgentCommandQueue.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentCommandQueue.cs)에서 우선순위와 인터럽트 여부를 함께 보존하고, [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)는 이를 주기적으로 소진해 대화 중 새 지시가 들어와도 더 안정적으로 반영합니다.
- 컨텍스트 관리도 보강했습니다. [AgentToolResultBudget.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs), [AgentQueryContextBuilder.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs), [ChatModels.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/ChatModels.cs)가 tool result preview를 대화 메시지에 캐시해 긴 세션과 재질문에서도 같은 축약 결과를 재사용하도록 정리했습니다.
- 코드 탭의 언어 지원도 내장 중심으로 넓혔습니다. [CodeLanguageCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/CodeLanguageCatalog.cs)를 추가해 코드 분류, 시스템 프롬프트, LSP 언어 판정, 인덱싱 확장자를 한 카탈로그로 묶었고, 설정의 코드 탭에는 `지원 언어(LSP)``코드 탭 기본 지원`을 명시적으로 노출했습니다. 격리 환경에서는 외부 설치를 전제하지 않고 내장 분석과 로컬에 이미 있는 언어 서버만 활용합니다.
- PPT 생성 품질도 크게 올렸습니다. [PptxSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PptxSkill.cs)에 `executive_summary`, `recommendation`, `roadmap`, `comparison`, `kpi_dashboard` 레이아웃을 추가했고, [DocumentPlannerTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs)는 발표용 문서 계획을 `Executive Summary -> Situation & Imperative -> Key Findings -> Options & Recommendation -> Implementation Roadmap -> Impact & Ask` 구조로 생성하도록 바꿨습니다. [pptx-creator.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/pptx-creator.skill.md)도 Python 우선 흐름에서 AX native `pptx_create` 중심으로 재작성했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_impl\\ -p:IntermediateOutputPath=obj\\verify_impl\\` 경고 0 / 오류 0
- 검증: `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\\` 통과 15
- 업데이트: 2026-04-14 19:16 (KST) - 업데이트: 2026-04-14 19:16 (KST)
- 분석용 로그 저장 방식을 롤링 형태로 정리했습니다. `app`, `perf`, `audit`, `workflow` 로그는 이제 날짜별 파일을 유지하되 각 파일이 최대 1MB를 넘지 않도록 오래된 내용부터 밀어내며 새 로그를 이어 붙입니다. - 분석용 로그 저장 방식을 롤링 형태로 정리했습니다. `app`, `perf`, `audit`, `workflow` 로그는 이제 날짜별 파일을 유지하되 각 파일이 최대 1MB를 넘지 않도록 오래된 내용부터 밀어내며 새 로그를 이어 붙입니다.
- 보관 정책도 같이 정리했습니다. 공통 로그/성능 로그/감사 로그는 14일까지만 유지하고, 워크플로우 상세 로그는 기존 설정값을 따르되 최대 14일을 넘지 않게 제한합니다. - 보관 정책도 같이 정리했습니다. 공통 로그/성능 로그/감사 로그는 14일까지만 유지하고, 워크플로우 상세 로그는 기존 설정값을 따르되 최대 14일을 넘지 않게 제한합니다.

File diff suppressed because it is too large Load Diff

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); var result = WorkspaceContextGenerator.LoadContext(tempDir);
result.Should().NotBeNull(); result.Should().NotBeNull();
result!.Should().Contain("(truncated)"); result!.Should().Contain("(truncated)");
result.Length.Should().BeLessOrEqualTo(4100); // 4000 + truncation message result!.Length.Should().BeLessOrEqualTo(4100); // 4000 + truncation message
} }
finally finally
{ {

View File

@@ -286,6 +286,13 @@ public class ChatMessage
/// <summary>첨부된 이미지 목록. base64 인코딩된 이미지 데이터.</summary> /// <summary>첨부된 이미지 목록. base64 인코딩된 이미지 데이터.</summary>
[JsonPropertyName("images")] [JsonPropertyName("images")]
public List<ImageAttachment>? Images { get; set; } public List<ImageAttachment>? Images { get; set; }
/// <summary>
/// 긴 tool_result를 query view에서 안정적으로 재사용하기 위한 축약본.
/// 원본 content는 유지하고, 모델에 보낼 때만 이 preview를 우선 사용합니다.
/// </summary>
[JsonPropertyName("queryPreviewContent")]
public string? QueryPreviewContent { get; set; }
} }
public class ChatExecutionEvent public class ChatExecutionEvent

View File

@@ -0,0 +1,107 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace AxCopilot.Services.Agent;
public enum AgentQueuePriority
{
Now = 0,
Next = 1,
Later = 2,
}
public enum AgentCommandKind
{
Prompt,
Notification,
}
public sealed record AgentQueuedCommand(
long Sequence,
AgentCommandKind Kind,
AgentQueuePriority Priority,
string Content,
DateTime CreatedAt,
bool RequestInterrupt);
/// <summary>
/// 에이전트 실행 중 추가 입력과 내부 알림을 관리하는 경량 큐.
/// UI 구조는 유지하면서도 우선순위/종류를 명확히 구분해
/// 실행 중 메시지 유입을 안정적으로 처리하도록 돕습니다.
/// </summary>
public sealed class AgentCommandQueue
{
private readonly ConcurrentQueue<AgentQueuedCommand> _now = new();
private readonly ConcurrentQueue<AgentQueuedCommand> _next = new();
private readonly ConcurrentQueue<AgentQueuedCommand> _later = new();
private long _sequence;
public void EnqueuePrompt(string content, string priority = "next", bool requestInterrupt = false)
=> Enqueue(AgentCommandKind.Prompt, content, priority, requestInterrupt);
public void EnqueueNotification(string content, string priority = "later")
=> Enqueue(AgentCommandKind.Notification, content, priority, requestInterrupt: false);
public void Clear()
{
while (_now.TryDequeue(out _)) { }
while (_next.TryDequeue(out _)) { }
while (_later.TryDequeue(out _)) { }
}
public List<AgentQueuedCommand> DrainAll()
{
var items = new List<AgentQueuedCommand>();
DrainInto(_now, items);
DrainInto(_next, items);
DrainInto(_later, items);
return items
.OrderBy(x => x.Priority)
.ThenBy(x => x.Sequence)
.ToList();
}
private void Enqueue(AgentCommandKind kind, string content, string? priority, bool requestInterrupt)
{
if (string.IsNullOrWhiteSpace(content))
return;
var item = new AgentQueuedCommand(
Interlocked.Increment(ref _sequence),
kind,
NormalizePriority(priority),
content.Trim(),
DateTime.Now,
requestInterrupt);
switch (item.Priority)
{
case AgentQueuePriority.Now:
_now.Enqueue(item);
break;
case AgentQueuePriority.Later:
_later.Enqueue(item);
break;
default:
_next.Enqueue(item);
break;
}
}
private static void DrainInto(ConcurrentQueue<AgentQueuedCommand> queue, List<AgentQueuedCommand> target)
{
while (queue.TryDequeue(out var item))
target.Add(item);
}
private static AgentQueuePriority NormalizePriority(string? priority)
=> priority?.Trim().ToLowerInvariant() switch
{
"now" => AgentQueuePriority.Now,
"later" => AgentQueuePriority.Later,
_ => AgentQueuePriority.Next,
};
}

View File

@@ -36,14 +36,64 @@ public partial class AgentLoopService
}; };
private readonly ConcurrentDictionary<string, PermissionPromptPreview> _pendingPermissionPreviews = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, PermissionPromptPreview> _pendingPermissionPreviews = new(StringComparer.OrdinalIgnoreCase);
/// <summary>실행 중 사용자 메시지 주입 큐 (Claude Code 스타일 mid-execution steering).</summary> /// <summary>실행 중 추가 입력/알림 큐. 우선순위와 종류를 함께 보존합니다.</summary>
private readonly ConcurrentQueue<string> _pendingUserMessages = new(); private readonly AgentCommandQueue _pendingCommands = new();
/// <summary>실행 중인 에이전트 루프에 사용자 메시지를 주입합니다. 다음 LLM 호출 전에 반영됩니다.</summary> /// <summary>실행 중인 에이전트 루프에 사용자 메시지를 주입합니다. 다음 LLM 호출 전에 반영됩니다.</summary>
public void InjectUserMessage(string message) public void InjectUserMessage(string message)
{ {
if (!string.IsNullOrWhiteSpace(message)) if (!string.IsNullOrWhiteSpace(message))
_pendingUserMessages.Enqueue(message); _pendingCommands.EnqueuePrompt(
message,
IsRunning ? "now" : "next",
requestInterrupt: IsRunning);
}
private void DrainPendingCommands(List<ChatMessage> messages)
{
var drained = _pendingCommands.DrainAll();
if (drained.Count == 0)
return;
var interruptingPrompts = drained.Count(x => x.Kind == AgentCommandKind.Prompt && x.RequestInterrupt);
if (interruptingPrompts > 0)
{
messages.Add(new ChatMessage
{
Role = "system",
MetaKind = "queued_input_interrupt",
Content = $"[queued input] {interruptingPrompts} new prompt(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;
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;
}
}
} }
/// <summary>에이전트 이벤트 스트림 (UI 바인딩용).</summary> /// <summary>에이전트 이벤트 스트림 (UI 바인딩용).</summary>
@@ -179,7 +229,7 @@ public partial class AgentLoopService
_currentRunId = Guid.NewGuid().ToString("N"); _currentRunId = Guid.NewGuid().ToString("N");
_docFallbackAttempted = false; _docFallbackAttempted = false;
_documentPlanApproved = false; _documentPlanApproved = false;
while (_pendingUserMessages.TryDequeue(out _)) { } // 이전 실행의 잔여 메시지 제거 _pendingCommands.Clear(); // 이전 실행의 잔여 제거
var llm = _settings.Settings.Llm; var llm = _settings.Settings.Llm;
var baseMax = llm.MaxAgentIterations > 0 ? llm.MaxAgentIterations : 25; var baseMax = llm.MaxAgentIterations > 0 ? llm.MaxAgentIterations : 25;
var maxIterations = baseMax; // 동적 조정 가능 var maxIterations = baseMax; // 동적 조정 가능
@@ -440,11 +490,7 @@ public partial class AgentLoopService
} }
// ── 실행 중 사용자 메시지 주입 (Claude Code 스타일 steering) ── // ── 실행 중 사용자 메시지 주입 (Claude Code 스타일 steering) ──
while (_pendingUserMessages.TryDequeue(out var injectedMsg)) DrainPendingCommands(messages);
{
messages.Add(new ChatMessage { Role = "user", Content = injectedMsg });
EmitEvent(AgentEventType.UserMessage, "", injectedMsg);
}
var queryView = AgentQueryContextBuilder.Build(messages); var queryView = AgentQueryContextBuilder.Build(messages);
var queryMessages = queryView.Messages; var queryMessages = queryView.Messages;
@@ -455,6 +501,7 @@ public partial class AgentLoopService
$"start={queryView.WindowStartIndex}, " + $"start={queryView.WindowStartIndex}, " +
$"pairs={queryView.PreservedToolPairCount}, " + $"pairs={queryView.PreservedToolPairCount}, " +
$"tool_result_budget={queryView.TruncatedToolResultCount}, " + $"tool_result_budget={queryView.TruncatedToolResultCount}, " +
$"tool_result_preview_reuse={queryView.ReusedToolResultPreviewCount}, " +
$"tokens {queryView.TokensBeforeBudget}->{queryView.TokensAfterBudget}"; $"tokens {queryView.TokensBeforeBudget}->{queryView.TokensAfterBudget}";
WorkflowLogService.LogTransition( WorkflowLogService.LogTransition(
_conversationId, _conversationId,

View File

@@ -12,6 +12,7 @@ public sealed class AgentQueryContextWindowResult
public bool ToolPairExpanded { get; init; } public bool ToolPairExpanded { get; init; }
public int PreservedToolPairCount { get; init; } public int PreservedToolPairCount { get; init; }
public int TruncatedToolResultCount { get; init; } public int TruncatedToolResultCount { get; init; }
public int ReusedToolResultPreviewCount { get; init; }
public int TokensBeforeBudget { get; init; } public int TokensBeforeBudget { get; init; }
public int TokensAfterBudget { get; init; } public int TokensAfterBudget { get; init; }
} }
@@ -38,6 +39,7 @@ public static class AgentQueryContextBuilder
ToolPairExpanded = false, ToolPairExpanded = false,
PreservedToolPairCount = 0, PreservedToolPairCount = 0,
TruncatedToolResultCount = 0, TruncatedToolResultCount = 0,
ReusedToolResultPreviewCount = 0,
TokensBeforeBudget = 0, TokensBeforeBudget = 0,
TokensAfterBudget = 0, TokensAfterBudget = 0,
}; };
@@ -65,7 +67,7 @@ public static class AgentQueryContextBuilder
InjectPostCompactContextMessage(windowMessages); InjectPostCompactContextMessage(windowMessages);
var tokensBeforeBudget = TokenEstimator.EstimateMessages(windowMessages); var tokensBeforeBudget = TokenEstimator.EstimateMessages(windowMessages);
var budgetResult = AgentToolResultBudget.Apply(windowMessages, ProtectedRecentNonSystemMessages); var budgetResult = AgentToolResultBudget.Apply(windowMessages, ProtectedRecentNonSystemMessages, sourceMessages: sourceMessages);
var tokensAfterBudget = TokenEstimator.EstimateMessages(windowMessages); var tokensAfterBudget = TokenEstimator.EstimateMessages(windowMessages);
return new AgentQueryContextWindowResult return new AgentQueryContextWindowResult
@@ -78,6 +80,7 @@ public static class AgentQueryContextBuilder
ToolPairExpanded = toolPairExpanded, ToolPairExpanded = toolPairExpanded,
PreservedToolPairCount = preservedToolPairs, PreservedToolPairCount = preservedToolPairs,
TruncatedToolResultCount = budgetResult.TruncatedCount, TruncatedToolResultCount = budgetResult.TruncatedCount,
ReusedToolResultPreviewCount = budgetResult.ReusedPreviewCount,
TokensBeforeBudget = tokensBeforeBudget, TokensBeforeBudget = tokensBeforeBudget,
TokensAfterBudget = tokensAfterBudget, TokensAfterBudget = tokensAfterBudget,
}; };
@@ -130,6 +133,7 @@ public static class AgentQueryContextBuilder
PromptTokens = source.PromptTokens, PromptTokens = source.PromptTokens,
CompletionTokens = source.CompletionTokens, CompletionTokens = source.CompletionTokens,
AttachedFiles = source.AttachedFiles?.ToList(), AttachedFiles = source.AttachedFiles?.ToList(),
QueryPreviewContent = source.QueryPreviewContent,
Images = source.Images?.Select(image => new ImageAttachment Images = source.Images?.Select(image => new ImageAttachment
{ {
Base64 = image.Base64, Base64 = image.Base64,

View File

@@ -8,6 +8,7 @@ public sealed class AgentToolResultBudgetResult
public int TruncatedCount { get; set; } public int TruncatedCount { get; set; }
public int ProcessedCount { get; set; } public int ProcessedCount { get; set; }
public int TotalCharsBefore { get; set; } public int TotalCharsBefore { get; set; }
public int ReusedPreviewCount { get; set; }
} }
/// <summary> /// <summary>
@@ -22,9 +23,13 @@ public static class AgentToolResultBudget
List<ChatMessage> messages, List<ChatMessage> messages,
int protectedRecentNonSystemMessages, int protectedRecentNonSystemMessages,
int softCharLimit = DefaultSoftCharLimit, int softCharLimit = DefaultSoftCharLimit,
int aggregateBudgetChars = DefaultAggregateBudgetChars) int aggregateBudgetChars = DefaultAggregateBudgetChars,
IReadOnlyList<ChatMessage>? sourceMessages = null)
{ {
var result = new AgentToolResultBudgetResult(); var result = new AgentToolResultBudgetResult();
var sourceById = sourceMessages?
.Where(message => !string.IsNullOrWhiteSpace(message.MsgId))
.ToDictionary(message => message.MsgId, StringComparer.OrdinalIgnoreCase);
var nonSystemIndexes = messages var nonSystemIndexes = messages
.Select((message, index) => new { message, index }) .Select((message, index) => new { message, index })
.Where(x => !string.Equals(x.message.Role, "system", StringComparison.OrdinalIgnoreCase)) .Where(x => !string.Equals(x.message.Role, "system", StringComparison.OrdinalIgnoreCase))
@@ -49,6 +54,15 @@ public static class AgentToolResultBudget
result.ProcessedCount++; result.ProcessedCount++;
result.TotalCharsBefore += content.Length; result.TotalCharsBefore += content.Length;
if (!string.IsNullOrWhiteSpace(message.QueryPreviewContent))
{
messages[i] = CloneMessage(message, message.QueryPreviewContent);
result.TruncatedCount++;
result.ReusedPreviewCount++;
continue;
}
spentChars += content.Length; spentChars += content.Length;
if (content.Length <= softCharLimit && spentChars <= aggregateBudgetChars) if (content.Length <= softCharLimit && spentChars <= aggregateBudgetChars)
continue; continue;
@@ -57,6 +71,14 @@ public static class AgentToolResultBudget
if (string.Equals(truncated, content, StringComparison.Ordinal)) if (string.Equals(truncated, content, StringComparison.Ordinal))
continue; continue;
message.QueryPreviewContent = truncated;
if (sourceById != null
&& sourceById.TryGetValue(message.MsgId, out var source)
&& string.IsNullOrWhiteSpace(source.QueryPreviewContent))
{
source.QueryPreviewContent = truncated;
}
messages[i] = CloneMessage(message, truncated); messages[i] = CloneMessage(message, truncated);
result.TruncatedCount++; result.TruncatedCount++;
} }
@@ -147,6 +169,7 @@ public static class AgentToolResultBudget
PromptTokens = source.PromptTokens, PromptTokens = source.PromptTokens,
CompletionTokens = source.CompletionTokens, CompletionTokens = source.CompletionTokens,
AttachedFiles = source.AttachedFiles?.ToList(), AttachedFiles = source.AttachedFiles?.ToList(),
QueryPreviewContent = source.QueryPreviewContent,
Images = source.Images?.Select(image => new ImageAttachment Images = source.Images?.Select(image => new ImageAttachment
{ {
Base64 = image.Base64, Base64 = image.Base64,

View File

@@ -616,6 +616,15 @@ public class DocumentPlannerTool : IAgentTool
new() { Id = "sec-4", Heading = "4. 액션 아이템", Level = 1, KeyPoints = ["담당자", "기한", "세부 내용"] }, new() { Id = "sec-4", Heading = "4. 액션 아이템", Level = 1, KeyPoints = ["담당자", "기한", "세부 내용"] },
new() { Id = "sec-5", Heading = "5. 다음 회의", Level = 1, KeyPoints = ["예정일", "주요 안건"] }, new() { Id = "sec-5", Heading = "5. 다음 회의", Level = 1, KeyPoints = ["예정일", "주요 안건"] },
}, },
"presentation" => new List<SectionPlan>
{
new() { Id = "sec-1", Heading = "1. Executive Summary", Level = 1, KeyPoints = ["핵심 메시지", "의사결정 포인트", "한 줄 권고안"] },
new() { Id = "sec-2", Heading = "2. Situation & Imperative", Level = 1, KeyPoints = ["배경", "왜 지금 중요한가", "문제 정의"] },
new() { Id = "sec-3", Heading = "3. Key Findings", Level = 1, KeyPoints = ["핵심 데이터", "주요 인사이트", "시사점"] },
new() { Id = "sec-4", Heading = "4. Options & Recommendation", Level = 1, KeyPoints = ["대안 비교", "추천안", "선택 근거"] },
new() { Id = "sec-5", Heading = "5. Implementation Roadmap", Level = 1, KeyPoints = ["단계", "일정", "책임 주체"] },
new() { Id = "sec-6", Heading = "6. Impact & Ask", Level = 1, KeyPoints = ["기대 효과", "핵심 KPI", "의사결정 요청 사항"] },
},
_ => new List<SectionPlan> _ => new List<SectionPlan>
{ {
new() { Id = "sec-1", Heading = "1. 개요", Level = 1, KeyPoints = ["배경", "목적", "범위"] }, new() { Id = "sec-1", Heading = "1. 개요", Level = 1, KeyPoints = ["배경", "목적", "범위"] },

View File

@@ -22,6 +22,7 @@ public class LspTool : IAgentTool, IDisposable
"- action=\"prepare_call_hierarchy\": 현재 위치의 호출 계층 기준 심볼을 확인합니다\n" + "- action=\"prepare_call_hierarchy\": 현재 위치의 호출 계층 기준 심볼을 확인합니다\n" +
"- action=\"incoming_calls\": 현재 심볼을 호출하는 상위 호출자를 찾습니다\n" + "- action=\"incoming_calls\": 현재 심볼을 호출하는 상위 호출자를 찾습니다\n" +
"- action=\"outgoing_calls\": 현재 심볼이 호출하는 하위 호출 대상을 찾습니다\n" + "- action=\"outgoing_calls\": 현재 심볼이 호출하는 하위 호출 대상을 찾습니다\n" +
$"지원 언어 서버: {Services.CodeLanguageCatalog.BuildLspSupportDescription()}\n" +
"line/character는 기본적으로 1-based 입력을 기대하며, 0-based 값도 호환 처리합니다."; "line/character는 기본적으로 1-based 입력을 기대하며, 0-based 값도 호환 처리합니다.";
public ToolParameterSchema Parameters => new() public ToolParameterSchema Parameters => new()
@@ -298,19 +299,7 @@ public class LspTool : IAgentTool, IDisposable
} }
private static string? DetectLanguage(string filePath) private static string? DetectLanguage(string filePath)
{ => Services.CodeLanguageCatalog.DetectLspLanguageId(filePath);
var ext = Path.GetExtension(filePath).ToLowerInvariant();
return ext switch
{
".cs" => "csharp",
".ts" or ".tsx" => "typescript",
".js" or ".jsx" => "javascript",
".py" => "python",
".cpp" or ".cc" or ".cxx" or ".c" or ".h" or ".hpp" => "cpp",
".java" => "java",
_ => null
};
}
private static int NormalizePosition(int value) private static int NormalizePosition(int value)
{ {

View File

@@ -36,6 +36,10 @@ public class PptxSkill : IAgentTool
"section (chapter divider), two_column (title+left+right), " + "section (chapter divider), two_column (title+left+right), " +
"table (title+real styled table), quote (styled quotation slide), blank, " + "table (title+real styled table), quote (styled quotation slide), blank, " +
"image_full (full-slide background image), " + "image_full (full-slide background image), " +
"executive_summary (consulting-style headline and takeaways), " +
"recommendation (single recommendation with rationale), " +
"roadmap (phase timeline), comparison (option comparison cards), " +
"kpi_dashboard (metric cards and takeaways), " +
"chart (native OpenXML chart — bar/line/pie with embedded data). " + "chart (native OpenXML chart — bar/line/pie with embedded data). " +
"Built-in themes (each controls BOTH colors AND layout/composition): " + "Built-in themes (each controls BOTH colors AND layout/composition): " +
"professional (bar_left), modern (top_band+card), dark (center_bold), " + "professional (bar_left), modern (top_band+card), dark (center_bold), " +
@@ -72,10 +76,17 @@ public class PptxSkill : IAgentTool
Type = "array", Type = "array",
Description = Description =
"Array of slide objects. Each slide: " + "Array of slide objects. Each slide: " +
"{\"layout\": \"title|content|section|two_column|table|quote|blank|image_full|chart\", " + "{\"layout\": \"title|content|section|two_column|table|quote|blank|image_full|chart|executive_summary|recommendation|roadmap|comparison|kpi_dashboard\", " +
"\"title\": \"...\", \"subtitle\": \"...\", " + "\"title\": \"...\", \"subtitle\": \"...\", " +
"\"body\": \"bullet1\\nbullet2\\n - sub-bullet (IMPORTANT: write 5-8 detailed bullet points per slide to fill the space fully)\", " + "\"headline\": \"single-message headline for the slide\", " +
"\"body\": \"3-5 concise evidence-backed bullets or short statements\", " +
"\"left\": \"...\", \"right\": \"...\", " + "\"left\": \"...\", \"right\": \"...\", " +
"\"summary_points\": [\"key point 1\", \"key point 2\"], " +
"\"recommendation\": \"recommended action or decision\", " +
"\"next_steps\": [\"step 1\", \"step 2\"], " +
"\"phases\": [{\"title\":\"Phase 1\",\"detail\":\"Design\",\"timeline\":\"Q1\",\"owner\":\"PM\"}], " +
"\"options\": [{\"name\":\"Option A\",\"pros\":\"...\",\"cons\":\"...\",\"verdict\":\"Recommended\"}], " +
"\"kpis\": [{\"label\":\"Revenue\",\"value\":\"12%\",\"trend\":\"YoY\",\"note\":\"Top-line growth\"}], " +
"\"headers\": [\"col1\",\"col2\"], \"rows\": [[\"a\",\"b\"]], " + "\"headers\": [\"col1\",\"col2\"], \"rows\": [[\"a\",\"b\"]], " +
"\"quote\": \"Quote text\", \"author\": \"Author\", " + "\"quote\": \"Quote text\", \"author\": \"Author\", " +
"\"image\": \"path/to/image.png (local file, embedded in slide; position: top_right|bottom_right|center|full|left_half|right_half; default: center)\", " + "\"image\": \"path/to/image.png (local file, embedded in slide; position: top_right|bottom_right|center|full|left_half|right_half; default: center)\", " +
@@ -818,6 +829,21 @@ public class PptxSkill : IAgentTool
case "chart": case "chart":
BuildChartSlide(slidePart, shapeTree, slideEl, fullTheme, slideW, slideH, ref sid); BuildChartSlide(slidePart, shapeTree, slideEl, fullTheme, slideW, slideH, ref sid);
break; break;
case "executive_summary":
BuildExecutiveSummarySlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid);
break;
case "recommendation":
BuildRecommendationSlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid);
break;
case "roadmap":
BuildRoadmapSlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid);
break;
case "comparison":
BuildComparisonSlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid);
break;
case "kpi_dashboard":
BuildKpiDashboardSlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid);
break;
case "blank": case "blank":
break; break;
default: // content default: // content
@@ -1432,6 +1458,334 @@ public class PptxSkill : IAgentTool
/// bar(세로 막대), line(꺾은선), pie(원형) 차트를 지원합니다. /// bar(세로 막대), line(꺾은선), pie(원형) 차트를 지원합니다.
/// ChartPart에 내장 스프레드시트 데이터를 포함하므로 PowerPoint에서 편집 가능합니다. /// ChartPart에 내장 스프레드시트 데이터를 포함하므로 PowerPoint에서 편집 가능합니다.
/// </summary> /// </summary>
private sealed record PresentationCardItem(string Title, string Primary, string Secondary, string Badge);
private static void BuildExecutiveSummarySlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id)
{
var c = theme.Colors;
const long M = 360000;
AddRect(t, ref id, 0, 0, W, H, c.Bg);
AddRect(t, ref id, 0, 0, W, 170000, c.Accent);
var title = CoalesceText(Str(s, "title"), "Executive Summary");
var headline = CoalesceText(Str(s, "headline"), Str(s, "subtitle"), title);
var summaryPoints = GetStringList(s, "summary_points");
if (summaryPoints.Count == 0)
summaryPoints = GetStringList(s, "body");
if (summaryPoints.Count == 0)
summaryPoints = ["핵심 메시지를 3개 이하로 정리", "가장 중요한 수치와 근거를 포함", "의사결정자가 다음 행동을 바로 이해할 수 있도록 구성"];
var recommendation = CoalesceText(Str(s, "recommendation"), Str(s, "subtitle"), "권고안과 기대효과를 한 문장으로 정리");
var kpis = GetStructuredItems(s, "kpis");
AddText(t, ref id, M, 220000, W - M * 2, 420000, title, 2200, c.Accent, bold: true, align: "l");
AddText(t, ref id, M, 520000, W - M * 2, 760000, headline, 3200, c.Primary, bold: true, align: "l");
long leftX = M;
long topY = 1420000;
long gap = 220000;
long leftW = (W - M * 2 - gap) * 58 / 100;
long rightX = leftX + leftW + gap;
long rightW = W - M - rightX;
long topH = 2300000;
AddRoundedRect(t, ref id, leftX, topY, leftW, topH, c.BgAlt);
AddText(t, ref id, leftX + 120000, topY + 70000, leftW - 240000, 300000, "Key Takeaways", 1800, c.Primary, bold: true, align: "l");
AddBulletBody(t, ref id, leftX + 100000, topY + 360000, leftW - 200000, topH - 460000,
string.Join("\n", summaryPoints.Select(x => $"- {x}")), 1700, c.TextDark, c.Accent);
AddRoundedRect(t, ref id, rightX, topY, rightW, topH, c.Primary);
AddText(t, ref id, rightX + 120000, topY + 70000, rightW - 240000, 260000, "Recommendation", 1800, c.TextLight, bold: true, align: "l");
AddTextEx(t, ref id, rightX + 120000, topY + 420000, rightW - 240000, 1100000, recommendation, 2200, c.TextLight, bold: true, italic: false, align: "l");
var rationaleLines = GetStringList(s, "next_steps");
if (rationaleLines.Count == 0)
rationaleLines = GetStringList(s, "body").Take(2).ToList();
if (rationaleLines.Count > 0)
AddBulletBody(t, ref id, rightX + 100000, topY + 1500000, rightW - 200000, topH - 1620000,
string.Join("\n", rationaleLines.Select(x => $"- {x}")), 1500, c.TextLight, c.Accent);
if (kpis.Count > 0)
{
var metricCount = Math.Min(3, kpis.Count);
long cardY = topY + topH + 200000;
long cardGap = 180000;
long cardW = (W - M * 2 - cardGap * (metricCount - 1)) / metricCount;
long cardH = 1050000;
for (int i = 0; i < metricCount; i++)
{
var metric = kpis[i];
var cardX = M + i * (cardW + cardGap);
AddRoundedRect(t, ref id, cardX, cardY, cardW, cardH, c.BgAlt);
AddText(t, ref id, cardX + 90000, cardY + 70000, cardW - 180000, 220000, metric.Title, 1400, c.TextDark, bold: false, align: "l");
AddText(t, ref id, cardX + 90000, cardY + 260000, cardW - 180000, 330000, CoalesceText(metric.Primary, "-"), 2600, c.Primary, bold: true, align: "l");
AddText(t, ref id, cardX + 90000, cardY + 640000, cardW - 180000, 170000, CoalesceText(metric.Secondary, metric.Badge), 1300, c.Accent, bold: true, align: "l");
}
}
}
private static void BuildRecommendationSlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id)
{
var c = theme.Colors;
const long M = 380000;
AddRect(t, ref id, 0, 0, W, H, c.Bg);
var title = CoalesceText(Str(s, "title"), "Recommendation");
var recommendation = CoalesceText(Str(s, "recommendation"), Str(s, "headline"), Str(s, "subtitle"), "권고안을 한 문장으로 제시");
var reasons = GetStringList(s, "summary_points");
if (reasons.Count == 0)
reasons = GetStringList(s, "body");
var nextSteps = GetStringList(s, "next_steps");
AddText(t, ref id, M, 220000, W - M * 2, 360000, title, 2200, c.Accent, bold: true, align: "l");
AddText(t, ref id, M, 520000, W - M * 2, 420000, recommendation, 3000, c.Primary, bold: true, align: "l");
long mainY = 1220000;
long gap = 220000;
long leftW = (W - M * 2 - gap) * 52 / 100;
long rightX = M + leftW + gap;
long rightW = W - M - rightX;
long boxH = 2450000;
AddRoundedRect(t, ref id, M, mainY, leftW, boxH, c.Primary);
AddText(t, ref id, M + 120000, mainY + 80000, leftW - 240000, 240000, "Recommended Move", 1800, c.TextLight, bold: true, align: "l");
AddTextEx(t, ref id, M + 120000, mainY + 400000, leftW - 240000, 1200000, recommendation, 2400, c.TextLight, bold: true, italic: false, align: "l");
if (nextSteps.Count > 0)
{
AddText(t, ref id, M + 120000, mainY + 1780000, leftW - 240000, 180000, "Immediate Actions", 1600, c.Accent, bold: true, align: "l");
AddBulletBody(t, ref id, M + 100000, mainY + 1990000, leftW - 200000, boxH - 2090000,
string.Join("\n", nextSteps.Take(3).Select(x => $"- {x}")), 1450, c.TextLight, c.Accent);
}
AddRoundedRect(t, ref id, rightX, mainY, rightW, boxH, c.BgAlt);
AddText(t, ref id, rightX + 120000, mainY + 80000, rightW - 240000, 240000, "Why This Wins", 1800, c.Primary, bold: true, align: "l");
AddBulletBody(t, ref id, rightX + 100000, mainY + 360000, rightW - 200000, boxH - 480000,
string.Join("\n", reasons.Take(5).Select(x => $"- {x}")), 1600, c.TextDark, c.Accent);
}
private static void BuildRoadmapSlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id)
{
var c = theme.Colors;
const long M = 360000;
AddRect(t, ref id, 0, 0, W, H, c.Bg);
var title = CoalesceText(Str(s, "title"), "Implementation Roadmap");
var headline = CoalesceText(Str(s, "headline"), Str(s, "subtitle"), "단계별 우선순위와 산출물을 한 눈에 정리");
var phases = GetStructuredItems(s, "phases");
if (phases.Count == 0)
{
phases =
[
new("Phase 1", "진단 및 설계", "0-30일", "PM"),
new("Phase 2", "구현 및 검증", "30-60일", "Delivery"),
new("Phase 3", "확산 및 운영정착", "60-90일", "Business")
];
}
AddText(t, ref id, M, 220000, W - M * 2, 320000, title, 2200, c.Accent, bold: true, align: "l");
AddText(t, ref id, M, 500000, W - M * 2, 420000, headline, 2800, c.Primary, bold: true, align: "l");
AddRect(t, ref id, M, 1260000, W - M * 2, 12000, c.Accent);
var phaseCount = Math.Min(4, phases.Count);
long gap = 160000;
long laneY = 1540000;
long laneW = (W - M * 2 - gap * (phaseCount - 1)) / phaseCount;
long laneH = 2100000;
for (int i = 0; i < phaseCount; i++)
{
var phase = phases[i];
var laneX = M + i * (laneW + gap);
var fill = i % 2 == 0 ? c.BgAlt : c.Bg;
AddRoundedRect(t, ref id, laneX, laneY, laneW, laneH, fill);
AddRect(t, ref id, laneX, laneY, laneW, 160000, i % 2 == 0 ? c.Primary : c.Accent);
AddText(t, ref id, laneX + 90000, laneY + 220000, laneW - 180000, 240000, phase.Title, 1800, c.Primary, bold: true, align: "l");
AddText(t, ref id, laneX + 90000, laneY + 520000, laneW - 180000, 520000, CoalesceText(phase.Primary, "핵심 과업 정의"), 1500, c.TextDark, bold: false, align: "l");
AddText(t, ref id, laneX + 90000, laneY + 1200000, laneW - 180000, 180000, CoalesceText(phase.Secondary, "기간 미정"), 1350, c.Accent, bold: true, align: "l");
AddText(t, ref id, laneX + 90000, laneY + 1450000, laneW - 180000, 180000, CoalesceText(phase.Badge, "담당 미정"), 1250, c.TextDark, bold: false, align: "l");
}
}
private static void BuildComparisonSlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id)
{
var c = theme.Colors;
const long M = 340000;
AddRect(t, ref id, 0, 0, W, H, c.Bg);
var title = CoalesceText(Str(s, "title"), "Option Comparison");
var headline = CoalesceText(Str(s, "headline"), Str(s, "subtitle"), "대안별 장단점과 권고안을 비교");
var options = GetStructuredItems(s, "options");
if (options.Count == 0)
{
options =
[
new("Option A", "빠른 적용", "확장성 제한", "Fastest"),
new("Option B", "균형 잡힌 투자", "의사결정 필요", "Recommended"),
new("Option C", "장기 최적화", "리드타임 길음", "Strategic")
];
}
AddText(t, ref id, M, 220000, W - M * 2, 320000, title, 2200, c.Accent, bold: true, align: "l");
AddText(t, ref id, M, 500000, W - M * 2, 420000, headline, 2800, c.Primary, bold: true, align: "l");
var optionCount = Math.Min(3, options.Count);
long gap = 160000;
long cardY = 1360000;
long cardH = 2600000;
long cardW = (W - M * 2 - gap * (optionCount - 1)) / optionCount;
for (int i = 0; i < optionCount; i++)
{
var option = options[i];
var cardX = M + i * (cardW + gap);
AddRoundedRect(t, ref id, cardX, cardY, cardW, cardH, c.BgAlt);
AddRect(t, ref id, cardX, cardY, cardW, 140000, i == 1 ? c.Accent : c.Primary);
AddText(t, ref id, cardX + 90000, cardY + 220000, cardW - 180000, 240000, option.Title, 1800, c.Primary, bold: true, align: "l");
AddText(t, ref id, cardX + 90000, cardY + 560000, cardW - 180000, 180000, "Pros", 1400, c.Accent, bold: true, align: "l");
AddTextEx(t, ref id, cardX + 90000, cardY + 760000, cardW - 180000, 520000, CoalesceText(option.Primary, "-"), 1450, c.TextDark, bold: false, italic: false, align: "l");
AddText(t, ref id, cardX + 90000, cardY + 1480000, cardW - 180000, 180000, "Risks", 1400, c.Accent, bold: true, align: "l");
AddTextEx(t, ref id, cardX + 90000, cardY + 1680000, cardW - 180000, 460000, CoalesceText(option.Secondary, "-"), 1450, c.TextDark, bold: false, italic: false, align: "l");
AddText(t, ref id, cardX + 90000, cardY + 2280000, cardW - 180000, 160000, CoalesceText(option.Badge, ""), 1300, c.Primary, bold: true, align: "l");
}
}
private static void BuildKpiDashboardSlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id)
{
var c = theme.Colors;
const long M = 360000;
AddRect(t, ref id, 0, 0, W, H, c.Bg);
var title = CoalesceText(Str(s, "title"), "KPI Dashboard");
var headline = CoalesceText(Str(s, "headline"), Str(s, "subtitle"), "핵심 지표의 현재 상태와 시사점");
var metrics = GetStructuredItems(s, "kpis");
if (metrics.Count == 0)
{
metrics =
[
new("Revenue", "12%", "YoY", "Strong"),
new("Cost", "-8%", "vs plan", "Improving"),
new("NPS", "61", "survey", "Stable"),
new("Delivery", "94%", "SLA", "On track")
];
}
AddText(t, ref id, M, 220000, W - M * 2, 320000, title, 2200, c.Accent, bold: true, align: "l");
AddText(t, ref id, M, 500000, W - M * 2, 420000, headline, 2800, c.Primary, bold: true, align: "l");
long cardTop = 1320000;
long gap = 180000;
long cardW = (W - M * 2 - gap) / 2;
long cardH = 1160000;
for (int i = 0; i < Math.Min(4, metrics.Count); i++)
{
var metric = metrics[i];
long row = i / 2;
long col = i % 2;
long x = M + col * (cardW + gap);
long y = cardTop + row * (cardH + 180000);
AddRoundedRect(t, ref id, x, y, cardW, cardH, c.BgAlt);
AddText(t, ref id, x + 100000, y + 80000, cardW - 200000, 180000, metric.Title, 1450, c.TextDark, bold: false, align: "l");
AddText(t, ref id, x + 100000, y + 280000, cardW - 200000, 340000, CoalesceText(metric.Primary, "-"), 2800, c.Primary, bold: true, align: "l");
AddText(t, ref id, x + 100000, y + 700000, cardW - 200000, 150000, CoalesceText(metric.Secondary, ""), 1300, c.Accent, bold: true, align: "l");
AddText(t, ref id, x + 100000, y + 900000, cardW - 200000, 140000, CoalesceText(metric.Badge, ""), 1200, c.TextDark, bold: false, align: "l");
}
var takeawayLines = GetStringList(s, "summary_points");
if (takeawayLines.Count == 0)
takeawayLines = GetStringList(s, "body");
if (takeawayLines.Count > 0)
{
AddRect(t, ref id, M, H - 650000, W - M * 2, 9000, c.Accent);
AddText(t, ref id, M, H - 560000, W - M * 2, 140000, "Implication", 1500, c.Primary, bold: true, align: "l");
AddTextEx(t, ref id, M, H - 380000, W - M * 2, 220000, takeawayLines[0], 1450, c.TextDark, bold: false, italic: false, align: "l");
}
}
private static List<string> GetStringList(JsonElement source, string propertyName)
{
if (!source.SafeTryGetProperty(propertyName, out var value))
return [];
if (value.ValueKind == JsonValueKind.Array)
{
return value.EnumerateArray()
.Select(item => item.ValueKind switch
{
JsonValueKind.String => item.SafeGetString() ?? string.Empty,
JsonValueKind.Object => CoalesceText(
FirstNonEmpty(item, "title", "name", "label", "value", "detail", "note"),
item.ToString()),
_ => item.ToString()
})
.Where(item => !string.IsNullOrWhiteSpace(item))
.ToList();
}
if (value.ValueKind == JsonValueKind.String)
{
return (value.SafeGetString() ?? string.Empty)
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(item => !string.IsNullOrWhiteSpace(item))
.ToList();
}
return [];
}
private static List<PresentationCardItem> GetStructuredItems(JsonElement source, string propertyName)
{
var items = new List<PresentationCardItem>();
if (!source.SafeTryGetProperty(propertyName, out var value) || value.ValueKind != JsonValueKind.Array)
return items;
foreach (var item in value.EnumerateArray())
{
if (item.ValueKind == JsonValueKind.String)
{
var text = item.SafeGetString() ?? string.Empty;
if (!string.IsNullOrWhiteSpace(text))
items.Add(new PresentationCardItem(text, string.Empty, string.Empty, string.Empty));
continue;
}
if (item.ValueKind != JsonValueKind.Object)
continue;
items.Add(new PresentationCardItem(
CoalesceText(FirstNonEmpty(item, "title", "name", "label"), "Item"),
CoalesceText(FirstNonEmpty(item, "value", "detail", "pros", "timeline", "summary"), string.Empty),
CoalesceText(FirstNonEmpty(item, "note", "cons", "trend", "owner", "secondary"), string.Empty),
CoalesceText(FirstNonEmpty(item, "badge", "verdict", "status", "tag"), string.Empty)));
}
return items;
}
private static string? FirstNonEmpty(JsonElement source, params string[] keys)
{
foreach (var key in keys)
{
if (!source.SafeTryGetProperty(key, out var value))
continue;
var text = value.ValueKind switch
{
JsonValueKind.String => value.SafeGetString(),
JsonValueKind.Number => value.ToString(),
_ => null
};
if (!string.IsNullOrWhiteSpace(text))
return text;
}
return null;
}
private static string CoalesceText(params string?[] values)
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? string.Empty;
private static void BuildChartSlide(SlidePart slidePart, ShapeTree tree, JsonElement s, private static void BuildChartSlide(SlidePart slidePart, ShapeTree tree, JsonElement s,
FullTheme theme, long W, long H, ref uint id) FullTheme theme, long W, long H, ref uint id)
{ {

View File

@@ -35,14 +35,7 @@ public class CodeIndexService : IDisposable
"import", "export", "function", "const", "let", "def", "self", "import", "export", "function", "const", "let", "def", "self",
}; };
private static readonly HashSet<string> CodeExtensions = new(StringComparer.OrdinalIgnoreCase) private static readonly IReadOnlyCollection<string> CodeExtensions = CodeLanguageCatalog.CodeExtensions;
{
".cs", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".cpp", ".c", ".h", ".hpp",
".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala",
".html", ".css", ".scss", ".json", ".xml", ".yaml", ".yml",
".md", ".txt", ".sql", ".sh", ".bat", ".ps1",
".csproj", ".sln", ".gradle", ".pom",
};
// ── DB 초기화 ─────────────────────────────────────────────────────── // ── DB 초기화 ───────────────────────────────────────────────────────

View File

@@ -0,0 +1,304 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
namespace AxCopilot.Services;
public sealed record CodeLanguageCapability(
string Key,
string DisplayName,
IReadOnlyList<string> Extensions,
IReadOnlyList<string> Guidance,
string? LspLanguageId = null,
bool ShowInQuickSelect = false,
string? QuickSelectKey = null,
string? QuickSelectLabel = null,
string? QuickSelectIcon = null);
/// <summary>
/// 코드 탭과 에이전트가 공통으로 참조하는 언어 지원 카탈로그.
/// - 파일 분류
/// - 인덱싱 대상 확장자
/// - 시스템 프롬프트 언어 가이드
/// - LSP 연동 가능 언어
/// - 설정 UI 설명 문자열
/// 를 한 곳에서 관리합니다.
/// </summary>
public static class CodeLanguageCatalog
{
private static readonly ReadOnlyCollection<CodeLanguageCapability> s_all =
new(new List<CodeLanguageCapability>
{
new(
"csharp",
"C# (.NET)",
[".cs", ".csx", ".csproj", ".sln"],
[
"Use dotnet CLI, solution/project files, and NuGet package conventions.",
"Follow Microsoft naming conventions and prefer targeted edits over broad rewrites.",
"Verify impact on callers, DI registration, nullable flow, and build configuration."
],
LspLanguageId: "csharp",
ShowInQuickSelect: true,
QuickSelectKey: "csharp",
QuickSelectLabel: "C# (.NET)",
QuickSelectIcon: "\uD83D\uDD39"),
new(
"python",
"Python",
[".py", ".pyi", ".ipynb"],
[
"Use pip/venv or conda only if already available in the environment.",
"Follow PEP 8, type hints, and module/package boundaries.",
"Prefer small focused functions and verify import/runtime errors after edits."
],
LspLanguageId: "python",
ShowInQuickSelect: true,
QuickSelectKey: "python",
QuickSelectLabel: "Python",
QuickSelectIcon: "\uD83D\uDC0D"),
new(
"java",
"Java",
[".java", ".gradle", ".pom"],
[
"Use Maven or Gradle conventions already present in the repository.",
"Follow package structure, visibility rules, and style consistent with the existing codebase.",
"Check interfaces, implementations, and test fixtures together when modifying shared behavior."
],
LspLanguageId: "java",
ShowInQuickSelect: true,
QuickSelectKey: "java",
QuickSelectLabel: "Java",
QuickSelectIcon: "\u2615"),
new(
"cpp",
"C / C++",
[".c", ".cc", ".cxx", ".cpp", ".h", ".hh", ".hpp", ".inl"],
[
"Respect the repository's existing build system, usually CMake, MSBuild, or compiler-specific scripts.",
"Be careful with headers, include order, ownership, ABI-sensitive changes, and platform guards.",
"Validate both declaration and implementation impact when editing shared types."
],
LspLanguageId: "cpp",
ShowInQuickSelect: true,
QuickSelectKey: "cpp",
QuickSelectLabel: "C/C++",
QuickSelectIcon: "\u2699"),
new(
"typescript",
"TypeScript",
[".ts", ".tsx", ".mts", ".cts"],
[
"Use the existing package manager and tsconfig structure.",
"Prefer explicit types on public boundaries and check build/lint config before changing module format.",
"Preserve framework conventions already used by the project."
],
LspLanguageId: "typescript"),
new(
"javascript",
"JavaScript / Vue",
[".js", ".jsx", ".mjs", ".cjs", ".vue"],
[
"Use the existing Node package manager and lint/format rules.",
"For Vue, preserve the current component style and API pattern used by the project.",
"Check module boundaries, imports, and runtime side effects after edits."
],
LspLanguageId: "javascript",
ShowInQuickSelect: true,
QuickSelectKey: "javascript",
QuickSelectLabel: "JavaScript / Vue",
QuickSelectIcon: "\uD83C\uDF10"),
new(
"go",
"Go",
[".go", ".mod", ".sum"],
[
"Preserve package boundaries, error-first flow, and gofmt-style formatting.",
"Check interfaces, exported identifiers, and concurrency-sensitive changes together."
]),
new(
"rust",
"Rust",
[".rs", ".toml"],
[
"Respect Cargo workspace structure, ownership/borrowing rules, and crate boundaries.",
"Prefer explicit enums/results and verify compiler diagnostics after edits."
]),
new(
"php",
"PHP",
[".php", ".phtml"],
[
"Follow the framework and autoloading structure already present in the project.",
"Be careful with runtime includes, container wiring, and mixed template/application files."
]),
new(
"ruby",
"Ruby",
[".rb", ".rake", ".gemspec"],
[
"Preserve gem structure, Rails or plain Ruby conventions already used in the repository.",
"Check dynamic dispatch, concerns/modules, and tests together after edits."
]),
new(
"kotlin",
"Kotlin",
[".kt", ".kts"],
[
"Preserve Gradle structure, package layout, and nullability intent.",
"Be careful with JVM interop boundaries and Android-specific module structure when present."
]),
new(
"swift",
"Swift",
[".swift"],
[
"Preserve target structure, Apple framework imports, and protocol-oriented design already in use.",
"Check app lifecycle and platform-specific behavior after edits."
]),
new(
"scala",
"Scala",
[".scala", ".sc"],
[
"Respect sbt/module structure, functional style, and existing typeclass or Akka patterns if present.",
"Keep public APIs simple and avoid unnecessary type-level churn."
]),
new(
"shell",
"Shell",
[".sh", ".bash", ".zsh"],
[
"Prefer safe quoting, explicit exit handling, and repository-local scripts over one-off inline shell.",
"Check portability assumptions and environment-specific commands."
]),
new(
"powershell",
"PowerShell",
[".ps1", ".psm1", ".psd1", ".bat", ".cmd"],
[
"Prefer native PowerShell cmdlets and safe path handling.",
"Be careful with Windows-specific side effects, quoting, and admin-sensitive operations."
]),
new(
"sql",
"SQL",
[".sql"],
[
"Preserve migration ordering, transactional safety, and index/constraint compatibility.",
"Call out destructive or data-migrating changes explicitly."
]),
new(
"web",
"HTML / CSS / SCSS",
[".html", ".htm", ".css", ".scss", ".sass", ".less", ".xaml"],
[
"Preserve the existing design system, layout structure, and accessibility semantics.",
"Prefer incremental visual changes and keep selectors/components scoped."
]),
new(
"markup",
"JSON / YAML / XML / Markdown",
[".json", ".jsonc", ".xml", ".yaml", ".yml", ".md", ".txt"],
[
"Preserve schema shape, indentation style, and comment/document conventions already used in the repository.",
"Validate references, keys, and generated consumer impact after edits."
]),
});
private static readonly ReadOnlyDictionary<string, CodeLanguageCapability> s_byKey =
new(s_all.ToDictionary(x => x.Key, StringComparer.OrdinalIgnoreCase));
private static readonly ReadOnlyDictionary<string, CodeLanguageCapability> s_byExtension =
new(s_all
.SelectMany(cap => cap.Extensions.Select(ext => new KeyValuePair<string, CodeLanguageCapability>(ext, cap)))
.ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase));
private static readonly HashSet<string> s_codeExtensions = new(
s_all.SelectMany(cap => cap.Extensions),
StringComparer.OrdinalIgnoreCase);
public static IReadOnlyList<CodeLanguageCapability> All => s_all;
public static IReadOnlyCollection<string> CodeExtensions => s_codeExtensions;
public static IReadOnlyList<CodeLanguageCapability> QuickSelectLanguages =>
s_all.Where(x => x.ShowInQuickSelect).ToList();
public static IReadOnlyList<CodeLanguageCapability> LspBackedLanguages =>
s_all.Where(x => !string.IsNullOrWhiteSpace(x.LspLanguageId)).ToList();
public static bool IsCodeLikeFile(string? extension)
=> !string.IsNullOrWhiteSpace(extension) && s_codeExtensions.Contains(extension);
public static CodeLanguageCapability? FindByKey(string? key)
{
if (string.IsNullOrWhiteSpace(key))
return null;
return s_byKey.TryGetValue(key.Trim(), out var found) ? found : null;
}
public static CodeLanguageCapability? FindByExtension(string? extension)
{
if (string.IsNullOrWhiteSpace(extension))
return null;
return s_byExtension.TryGetValue(extension.Trim(), out var found) ? found : null;
}
public static string? DetectLspLanguageId(string? filePath)
=> FindByExtension(Path.GetExtension(filePath ?? string.Empty))?.LspLanguageId;
public static string GetQuickSelectLabel(string? key)
=> FindByKey(key)?.DisplayName ?? key ?? "Auto";
public static string BuildSelectedLanguagePrompt(string? key)
{
if (string.IsNullOrWhiteSpace(key) || string.Equals(key, "auto", StringComparison.OrdinalIgnoreCase))
return string.Empty;
var capability = FindByKey(key);
if (capability == null)
return string.Empty;
return $"IMPORTANT: User selected language: {capability.DisplayName}. Prioritize this language for code analysis and generation.";
}
public static IEnumerable<string> GetGuidanceLines(string? selectedKey)
{
var selected = FindByKey(selectedKey);
if (selected != null)
{
foreach (var line in selected.Guidance)
yield return $"- {selected.DisplayName}: {line}";
yield break;
}
foreach (var capability in s_all.Where(x =>
x.Key is "csharp" or "python" or "java" or "cpp" or "typescript" or "javascript" or "go" or "rust" or "kotlin" or "swift"))
{
var summary = capability.Guidance.FirstOrDefault();
if (!string.IsNullOrWhiteSpace(summary))
yield return $"- {capability.DisplayName}: {summary}";
}
}
public static string BuildLspSupportDescription()
=> string.Join(", ", LspBackedLanguages.Select(x => x.DisplayName));
public static string BuildStaticSupportDescription()
=> string.Join(", ", s_all.Select(x => x.DisplayName));
public static string BuildCodeTabSupportDescription()
{
var sb = new StringBuilder();
sb.Append("정적 분류/검색/프롬프트 지원: ");
sb.Append(BuildStaticSupportDescription());
sb.Append(" | LSP 심화 분석: ");
sb.Append(BuildLspSupportDescription());
return sb.ToString();
}
}

View File

@@ -138,6 +138,9 @@ public class SettingsViewModel : INotifyPropertyChanged
/// <summary>CodeSettings 바인딩용 프로퍼티. XAML에서 {Binding Code.EnableLsp} 등으로 접근.</summary> /// <summary>CodeSettings 바인딩용 프로퍼티. XAML에서 {Binding Code.EnableLsp} 등으로 접근.</summary>
public Models.CodeSettings Code => _service.Settings.Llm.Code; public Models.CodeSettings Code => _service.Settings.Llm.Code;
public string CodeLspSupportedLanguagesText => CodeLanguageCatalog.BuildLspSupportDescription();
public string CodeStaticSupportedLanguagesText => CodeLanguageCatalog.BuildStaticSupportDescription();
public string CodeTabSupportSummaryText => CodeLanguageCatalog.BuildCodeTabSupportDescription();
// ─── 작업 복사본 ─────────────────────────────────────────────────────── // ─── 작업 복사본 ───────────────────────────────────────────────────────
private string _hotkey; private string _hotkey;

View File

@@ -281,19 +281,14 @@ public partial class ChatWindow
sb.AppendLine($"\nPreferred IDE: {code.PreferredIdePath}"); sb.AppendLine($"\nPreferred IDE: {code.PreferredIdePath}");
// 사용자 선택 개발 언어 // 사용자 선택 개발 언어
if (_selectedLanguage != "auto") var selectedLanguagePrompt = CodeLanguageCatalog.BuildSelectedLanguagePrompt(_selectedLanguage);
{ if (!string.IsNullOrWhiteSpace(selectedLanguagePrompt))
var langName = _selectedLanguage switch { "python" => "Python", "java" => "Java", "csharp" => "C# (.NET)", "cpp" => "C/C++", "javascript" => "JavaScript/TypeScript", _ => _selectedLanguage }; sb.AppendLine($"\n{selectedLanguagePrompt}");
sb.AppendLine($"\nIMPORTANT: User selected language: {langName}. Prioritize this language for code analysis and generation.");
}
// 언어별 가이드라인 // 언어별 가이드라인
sb.AppendLine("\n## Language Guidelines"); sb.AppendLine("\n## Language Guidelines");
sb.AppendLine("- C# (.NET): Use dotnet CLI. NuGet for packages. Follow Microsoft naming conventions."); foreach (var guidance in CodeLanguageCatalog.GetGuidanceLines(_selectedLanguage == "auto" ? null : _selectedLanguage))
sb.AppendLine("- Python: Use conda/pip. Follow PEP8. Use type hints. Virtual env preferred."); sb.AppendLine(guidance);
sb.AppendLine("- Java: Use Maven/Gradle. Follow Google Java Style Guide.");
sb.AppendLine("- C++: Use CMake for build. Follow C++ Core Guidelines.");
sb.AppendLine("- JavaScript/TypeScript: Use npm/yarn. Follow ESLint rules. Vue3 uses Composition API.");
// 코드 품질 + 안전 수칙 // 코드 품질 + 안전 수칙
sb.AppendLine("\n## Code Quality & Safety"); sb.AppendLine("\n## Code Quality & Safety");

View File

@@ -3655,8 +3655,7 @@ public partial class ChatWindow : Window
], ],
}; };
private static readonly HashSet<string> CodeExtensions = new(StringComparer.OrdinalIgnoreCase) private static readonly HashSet<string> CodeExtensions = new(Services.CodeLanguageCatalog.CodeExtensions, StringComparer.OrdinalIgnoreCase);
{ ".cs", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".cpp", ".c", ".h", ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala", ".sh", ".ps1", ".bat", ".cmd", ".sql", ".xaml", ".vue" };
private static readonly HashSet<string> DataExtensions = new(StringComparer.OrdinalIgnoreCase) private static readonly HashSet<string> DataExtensions = new(StringComparer.OrdinalIgnoreCase)
{ ".csv", ".json", ".xml", ".yaml", ".yml", ".tsv" }; { ".csv", ".json", ".xml", ".yaml", ".yml", ".tsv" };
// ImageExtensions는 이미지 첨부 영역(line ~1323)에서 정의됨 — 재사용 // ImageExtensions는 이미지 첨부 영역(line ~1323)에서 정의됨 — 재사용

View File

@@ -5458,7 +5458,7 @@
<LineBreak/>• 심볼 정의 위치 찾기 (Goto Definition) <LineBreak/>• 심볼 정의 위치 찾기 (Goto Definition)
<LineBreak/>• 심볼이 사용된 모든 위치 검색 (Find References) <LineBreak/>• 심볼이 사용된 모든 위치 검색 (Find References)
<LineBreak/>• 파일 내 클래스/메서드/필드 목록 조회 <LineBreak/>• 파일 내 클래스/메서드/필드 목록 조회
<LineBreak/><LineBreak/>지원 언어: C#, TypeScript, Python, C++, Java <LineBreak/><LineBreak/>지원 언어는 아래 설정 설명에 표시됩니다.
<LineBreak/>해당 언어 서버가 PC에 설치되어 있어야 동작합니다. <LineBreak/>해당 언어 서버가 PC에 설치되어 있어야 동작합니다.
</TextBlock> </TextBlock>
</ToolTip> </ToolTip>
@@ -5466,6 +5466,9 @@
</Border> </Border>
</StackPanel> </StackPanel>
<TextBlock Style="{StaticResource RowHint}" Text="언어 서버로 정의 이동, 참조 검색 등 코드 분석을 지원합니다"/> <TextBlock Style="{StaticResource RowHint}" Text="언어 서버로 정의 이동, 참조 검색 등 코드 분석을 지원합니다"/>
<TextBlock Style="{StaticResource RowHint}" Margin="0,4,0,0" Text="{Binding CodeLspSupportedLanguagesText, StringFormat=지원 언어(LSP): {0}}"/>
<TextBlock Style="{StaticResource RowHint}" Margin="0,2,0,0" Text="{Binding CodeStaticSupportedLanguagesText, StringFormat=코드 탭 기본 지원: {0}}"/>
<TextBlock Style="{StaticResource RowHint}" Margin="0,2,0,0" Text="격리 환경에서는 내장 분석과 로컬에 이미 설치된 언어 서버만 활용합니다"/>
</StackPanel> </StackPanel>
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center" <CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
IsChecked="{Binding Code.EnableLsp, Mode=TwoWay}"/> IsChecked="{Binding Code.EnableLsp, Mode=TwoWay}"/>

View File

@@ -1,125 +1,109 @@
--- ---
name: pptx-creator name: pptx-creator
label: PPT 프레젠테이션 생성 label: PPT 프레젠테이션 생성
description: Python을 사용하여 전문적인 PowerPoint 프레젠테이션을 생성합니다. 작업 폴더의 양식 파일을 자동 활용합니다. description: AX 기본 PPT 엔진으로 고급 제안서, 보고서, 발표 자료를 생성합니다. 작업 폴더의 양식 파일과 프로젝트 자료를 함께 활용합니다.
icon: \uE7BE icon: \uE7BE
when_to_use: 발표 자료, 제안서 deck, 보고용 슬라이드, 교육용 프레젠테이션을 새로 만들어야 하거나 기존 양식 PPT를 활용해야 할 때 when_to_use: 제안서 deck, 경영보고, 임원 보고자료, 프로젝트 현황 발표, 교육용 슬라이드, 회의 발표 자료를 새로 만들어야 하거나 기존 양식 PPT를 활용해야 할 때
argument-hint: <주제 또는 문서 목적> argument-hint: <주제 또는 문서 목적>
allowed-tools: allowed-tools:
- folder_map - folder_map
- document_plan
- document_read - document_read
- file_read - file_read
- file_write
- process
- pptx_create - pptx_create
- template_render
tabs: cowork tabs: cowork
--- ---
사용자의 요구에 맞는 PowerPoint 프레젠테이션을 Python으로 생성하세요. 사용자의 요구에 맞는 PowerPoint 프레젠테이션을 AX 기본 `pptx_create` 도구로 생성하세요.
## 실행 경로 선택 (Python 가능/불가)
- 먼저 `process``python --version`을 확인하세요.
- Python 가능: 기존 python-pptx 경로를 사용하세요.
- Python 불가: `pptx_create`로 슬라이드 초안을 생성하고 `template_render` + `file_write`로 발표자료 구조를 보강하세요.
## 기본 원칙
- 기본 경로는 항상 `pptx_create`입니다. Python 스크립트 생성은 예외 상황에서만 고려하세요.
- 슬라이드마다 메시지는 하나만 두고, 제목은 설명형 문장이 아니라 결론형 headline으로 작성하세요.
- bullet은 슬라이드당 3~5개 이내의 짧고 강한 문장으로 유지하세요. 공간을 채우기 위한 장문 bullet은 금지합니다.
- 숫자, 비교, 일정, 권고안은 각각 맞는 레이아웃으로 분리하세요.
## 사전 준비 ## 권장 작업 순서
필요한 패키지를 확인하고 설치하세요: 1. `folder_map`으로 작업 폴더를 확인하고 기존 `.pptx`, 보고서, 분석 문서, 회의록이 있는지 찾으세요.
``` 2. 참고할 양식 PPT가 있으면 파일명을 기억하고, 필요 시 `document_read`로 슬라이드 구조를 파악하세요.
process: pip install python-pptx 3. 먼저 `document_plan``document_type: presentation`으로 호출해서 스토리라인을 정리하세요.
4. 그 결과를 바탕으로 `pptx_create`용 슬라이드 배열을 설계하세요.
5. 생성 후 결과 파일 경로와 핵심 구성(슬라이드 수, 사용 템플릿/테마, 주요 레이아웃)을 간단히 안내하세요.
## 컨설팅형 스토리라인 규칙
- 기본 구조는 가능하면 아래 순서를 따르세요.
- `Executive Summary`
- `Situation & Imperative`
- `Key Findings`
- `Options & Recommendation`
- `Implementation Roadmap`
- `Impact & Ask`
## 레이아웃 선택 기준
- `executive_summary`: 경영진 요약, 한 줄 권고안, 핵심 takeaways, KPI 요약
- `recommendation`: 단일 권고안과 근거, 즉시 실행 항목
- `comparison`: 대안 비교, 옵션별 pros/risks, 추천안 강조
- `roadmap`: 단계별 일정, 소유자, 주요 산출물
- `kpi_dashboard`: 핵심 수치, 추세, 시사점
- `chart`: 정량 데이터 시각화
- `table`: 비교표, 상세 데이터표
- `section`: 장 구분
- `content`: 일반 설명 슬라이드
## 양식 활용
- 작업 폴더에 기존 양식 PPT가 있으면 `template` 또는 `theme_file`로 우선 활용하세요.
- 양식 후보 예시:
- 파일명에 `양식`, `template`, `표준`, `기본`, `보고`, `proposal`, `deck` 포함
- 사용자가 특정 `.pptx` 파일명을 직접 언급
- 양식이 명확하지 않으면 기본 `template: basic100` 또는 문서 성격에 맞는 `corporate`, `professional`, `modern` 중 하나를 사용하세요.
## 슬라이드 작성 규칙
- 제목은 `무엇을 다룰지`가 아니라 `무슨 결론인지`를 말해야 합니다.
- 각 슬라이드는 headline, supporting evidence, takeaway의 3요소를 갖추세요.
- 데이터가 없으면 억지 차트를 만들지 말고, 비교 카드나 권고안 슬라이드로 전환하세요.
- 표는 설명보다 비교에 유리할 때만 사용하세요.
- speaker notes가 유용하면 `notes`를 넣어 발표 포인트를 남기세요.
## 예시 흐름
- `document_plan`으로 발표 구조 생성
- `pptx_create`에서 아래와 같은 레이아웃 조합 사용
- `title`
- `executive_summary`
- `comparison`
- `recommendation`
- `roadmap`
- `kpi_dashboard`
- `section`
- `content`
## 예시 파라미터 스케치
```json
{
"path": "strategy-deck.pptx",
"template": "basic100",
"slides": [
{
"layout": "title",
"title": "2026 사업 전략 제안",
"subtitle": "성장 가속과 운영 효율 동시 달성"
},
{
"layout": "executive_summary",
"title": "Executive Summary",
"headline": "핵심 투자 우선순위를 재배치하면 2개 분기 내 수익성을 개선할 수 있습니다.",
"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": "핵심 세그먼트" }
]
}
]
}
``` ```
## 양식 활용 (마스터 슬라이드 템플릿) 한국어로 안내하고, 결과 파일은 작업 폴더에 저장하세요.
작업 폴더에 PPT 양식이 있으면 **반드시** 활용하세요:
1. **양식 탐색**: `folder_map`으로 작업 폴더를 스캔하여 `.pptx` 파일 확인
2. **양식 후보 판별**:
- 파일명에 "양식", "template", "서식", "표준", "기본" 포함
- 또는 사용자가 명시적으로 "XX 양식으로 작성해줘" 요청
- 또는 사용자가 특정 .pptx 파일명을 언급
3. **양식 구조 파악**: `document_read`로 양식의 슬라이드 레이아웃 목록 확인
4. **양식 기반 생성**:
```python
prs = Presentation('양식_발표.pptx')
# 마스터 슬라이드의 배경, 로고, 색 테마, 폰트가 자동 상속
# 기존 슬라이드 제거 후 새 내용 추가
while len(prs.slides) > 0:
rId = prs.slides._sldIdLst[0].rId
prs.part.drop_rel(rId)
del prs.slides._sldIdLst[0]
```
5. **레이아웃 확인** (양식마다 다를 수 있음):
```python
for i, layout in enumerate(prs.slide_layouts):
print(f'{i}: {layout.name}')
```
6. **양식이 없으면**: 아래 기본 템플릿으로 새 프레젠테이션 생성
## 작업 절차
1. **요구사항 파악**: 발표 주제, 슬라이드 수, 스타일 확인
2. **양식 확인**: folder_map으로 작업 폴더에 양식 .pptx 파일이 있는지 확인
3. **스크립트 작성**: file_write로 Python 스크립트 생성
4. **실행**: `process`로 스크립트 실행
5. **결과 안내**: 생성된 .pptx 파일 경로를 사용자에게 전달
## 스크립트 템플릿
```python
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
import os
# 양식 파일 자동 감지
template_keywords = ['양식', 'template', '서식', '표준', '기본']
template_file = None
for f in os.listdir('.'):
if f.endswith('.pptx') and any(kw in f.lower() for kw in template_keywords):
template_file = f
break
# 양식이 있으면 활용, 없으면 새 프레젠테이션
if template_file:
prs = Presentation(template_file)
# 기존 슬라이드 제거 (마스터/레이아웃은 유지)
while len(prs.slides) > 0:
rId = prs.slides._sldIdLst[0].rId
prs.part.drop_rel(rId)
del prs.slides._sldIdLst[0]
print(f'양식 활용: {template_file}')
print(f'사용 가능한 레이아웃: {[l.name for l in prs.slide_layouts]}')
else:
prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
# 제목 슬라이드
slide = prs.slides.add_slide(prs.slide_layouts[0])
slide.shapes.title.text = '프레젠테이션 제목'
if len(slide.placeholders) > 1:
slide.placeholders[1].text = '부제목'
# 내용 슬라이드
slide = prs.slides.add_slide(prs.slide_layouts[1])
slide.shapes.title.text = '섹션 제목'
body = slide.placeholders[1]
body.text = '첫 번째 포인트'
p = body.text_frame.add_paragraph()
p.text = '두 번째 포인트'
prs.save('presentation.pptx')
print('프레젠테이션 생성 완료: presentation.pptx')
```
## 지원 기능
- 제목/내용/빈 슬라이드 레이아웃
- 텍스트 서식 (글꼴, 크기, 색상, 정렬)
- 표 삽입
- 이미지 삽입
- 도형 (사각형, 원, 화살표)
- 차트 (막대, 선, 원형)
- 슬라이드 번호
- 마스터 슬라이드 커스터마이징
- **양식 파일 기반 마스터/레이아웃 상속** (배경, 로고, 색 테마, 폰트 자동 유지)
한국어로 안내하세요. 작업 폴더에 결과 파일을 저장하세요.