diff --git a/README.md b/README.md index 0b6899a..5e5965e 100644 --- a/README.md +++ b/README.md @@ -1262,3 +1262,7 @@ MIT License - 일정 시간마다 표시되는 격려문구 알림 팝업의 자동 닫힘 경로를 점검하고 [ReminderPopupWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ReminderPopupWindow.xaml.cs)를 보강했다. - 기존에는 `DispatcherTimer` 틱만으로 카운트다운과 닫힘을 함께 처리했는데, UI 틱이 밀리면 팝업 종료가 늦어질 여지가 있었다. 이제 카운트다운 표시와 실제 자동 종료를 분리해 `Task.Delay + CancellationToken` 기반 종료를 추가하고, 남은 시간은 절대 시각 기준으로 계산하도록 바꿨다. - 이 변경으로 지정 시간이 지난 뒤에도 격려 팝업이 남아 있는 증상을 줄이고, 자동 닫힘이 더 안정적으로 동작하도록 맞췄다. +- 업데이트: 2026-04-06 16:02 (KST) + - 코워크 문서 생성이 항상 비슷한 HTML로 수렴하던 원인을 줄이기 위해 [DocumentPlannerTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs), [DocumentAssemblerTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DocumentAssemblerTool.cs)의 기본 포맷/무드 선택 로직을 재정리했다. + - 이제 인자 없이 문서 생성 도구를 호출해도 무조건 `html + professional`로 고정되지 않고, `DefaultOutputFormat`, `DefaultMood`, 문서 유형(`proposal`, `analysis`, `manual`, `minutes` 등), 요청 주제 키워드를 함께 보고 `docx/html/markdown` 및 `corporate/dashboard/minimal/creative/professional`을 자동 선택한다. + - 이 변경으로 AX의 문서 생성 체인이 `claw-code`처럼 요청 기반 자유 작성 흐름에 더 가까워졌고, 코워크 결과물이 항상 비슷한 보고서형 HTML로 반복되던 현상을 줄일 기반을 마련했다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index cf2c739..13f5c47 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4967,3 +4967,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - Document update: 2026-04-06 15:41 (KST) - Reworked the empty-state layout in `ChatWindow.xaml` so the icon/title/description block and the preset grid live inside one vertically centered stack. This fixes the previous behavior where only the preset cards looked centered while the descriptive header stayed visually high on tall windows. - Document update: 2026-04-06 15:48 (KST) - Hardened the unlock-reminder popup auto-close path in `ReminderPopupWindow.xaml.cs`. The window now tracks an absolute close deadline and uses a separate `Task.Delay + CancellationToken` fail-safe to close the popup even if the UI timer tick is delayed. - Document update: 2026-04-06 15:48 (KST) - The countdown bar still updates through `DispatcherTimer`, but the actual close action is now guaranteed by the background delay path. This reduces cases where encouragement popups remain visible after the configured display duration. +- Document update: 2026-04-06 16:02 (KST) - Relaxed AX Cowork document-generation defaults so the planner/assembler path no longer collapses to `html + professional` whenever explicit arguments are omitted. `DocumentPlannerTool.cs` now resolves output format and mood from `Llm.DefaultOutputFormat`, `Llm.DefaultMood`, document type, and request keywords instead of hardcoding HTML/professional defaults. +- Document update: 2026-04-06 16:02 (KST) - `DocumentAssemblerTool.cs` now mirrors that resolution logic at assembly time, supporting `auto` as a first-class format input and inferring `docx/html/markdown` plus `corporate/dashboard/minimal/creative/professional` mood variants from title/section intent. This reduces repetitive HTML-style outputs and brings AX closer to the more request-driven document flow seen in `claw-code`. diff --git a/src/AxCopilot/Services/Agent/DocumentAssemblerTool.cs b/src/AxCopilot/Services/Agent/DocumentAssemblerTool.cs index 8abafe9..e32b044 100644 --- a/src/AxCopilot/Services/Agent/DocumentAssemblerTool.cs +++ b/src/AxCopilot/Services/Agent/DocumentAssemblerTool.cs @@ -10,6 +10,17 @@ namespace AxCopilot.Services.Agent; /// public class DocumentAssemblerTool : IAgentTool { + private static string GetDefaultOutputFormat() + { + var app = System.Windows.Application.Current as App; + return app?.SettingsService?.Settings.Llm.DefaultOutputFormat ?? "auto"; + } + + private static string GetDefaultMood() + { + var app = System.Windows.Application.Current as App; + return app?.SettingsService?.Settings.Llm.DefaultMood ?? "modern"; + } public string Name => "document_assemble"; public string Description => @@ -35,13 +46,13 @@ public class DocumentAssemblerTool : IAgentTool ["format"] = new() { Type = "string", - Description = "Output format: html, docx, markdown. Default: html", - Enum = ["html", "docx", "markdown"] + Description = "Output format: auto, html, docx, markdown. Default: auto (resolved from settings and request intent)", + Enum = ["auto", "html", "docx", "markdown"] }, ["mood"] = new() { Type = "string", - Description = "Design theme for HTML output: modern, professional, creative, corporate, dashboard, etc. Default: professional" + Description = "Design theme for HTML output: modern, professional, creative, corporate, dashboard, etc. Default: inferred from settings and document intent" }, ["toc"] = new() { Type = "boolean", Description = "Auto-generate table of contents. Default: true" }, ["cover_subtitle"] = new() { Type = "string", Description = "Subtitle for cover page. If provided, a cover page is added." }, @@ -55,8 +66,8 @@ public class DocumentAssemblerTool : IAgentTool { var path = args.GetProperty("path").GetString() ?? ""; var title = args.GetProperty("title").GetString() ?? "Document"; - var format = args.TryGetProperty("format", out var fmt) ? fmt.GetString() ?? "html" : "html"; - var mood = args.TryGetProperty("mood", out var m) ? m.GetString() ?? "professional" : "professional"; + var requestedFormat = args.TryGetProperty("format", out var fmt) ? fmt.GetString() ?? "auto" : GetDefaultOutputFormat(); + var requestedMood = args.TryGetProperty("mood", out var m) ? m.GetString() ?? GetDefaultMood() : GetDefaultMood(); var useToc = !args.TryGetProperty("toc", out var tocVal) || tocVal.GetBoolean(); // default true var coverSubtitle = args.TryGetProperty("cover_subtitle", out var cs) ? cs.GetString() : null; var headerText = args.TryGetProperty("header", out var hdr) ? hdr.GetString() : null; @@ -78,6 +89,9 @@ public class DocumentAssemblerTool : IAgentTool if (sections.Count == 0) return ToolResult.Fail("조립할 섹션이 없습니다."); + var format = ResolveDocumentFormat(requestedFormat, title, sections); + var mood = ResolveDocumentMood(requestedMood, title, sections); + var fullPath = FileReadTool.ResolvePath(path, context.WorkFolder); if (context.ActiveTab == "Cowork") fullPath = AgentContext.EnsureTimestampedPath(fullPath); @@ -318,6 +332,57 @@ public class DocumentAssemblerTool : IAgentTool return " ✓ Markdown 조립 완료"; } + private static string ResolveDocumentFormat(string? preferred, string title, List<(string Heading, string Content, int Level)> sections) + { + var normalized = (preferred ?? "auto").Trim().ToLowerInvariant(); + if (normalized is "html" or "docx" or "markdown") + return normalized; + + var intent = BuildIntentText(title, sections); + if (ContainsAny(intent, "docx", "word", "워드")) + return "docx"; + if (ContainsAny(intent, "markdown", ".md", "마크다운", "readme", "가이드", "매뉴얼", "회의록")) + return "markdown"; + if (ContainsAny(intent, "html", "웹 문서", "웹페이지", "대시보드")) + return "html"; + if (ContainsAny(intent, "proposal", "제안", "제안서", "보고")) + return "docx"; + if (ContainsAny(intent, "analysis", "분석", "지표", "통계")) + return "html"; + + return "docx"; + } + + private static string ResolveDocumentMood(string? preferred, string title, List<(string Heading, string Content, int Level)> sections) + { + var normalized = (preferred ?? "modern").Trim().ToLowerInvariant(); + if (!string.IsNullOrWhiteSpace(normalized) && normalized != "modern") + return normalized; + + var intent = BuildIntentText(title, sections); + if (ContainsAny(intent, "dashboard", "분석", "지표", "통계", "kpi")) + return "dashboard"; + if (ContainsAny(intent, "기획", "아이디어", "브레인스토밍", "creative")) + return "creative"; + if (ContainsAny(intent, "기업", "공식", "proposal", "제안서", "사내")) + return "corporate"; + if (ContainsAny(intent, "가이드", "매뉴얼", "manual", "guide")) + return "minimal"; + if (ContainsAny(intent, "회의록", "minutes")) + return "professional"; + + return "professional"; + } + + private static string BuildIntentText(string title, List<(string Heading, string Content, int Level)> sections) + { + var headingText = string.Join(" ", sections.Select(s => s.Heading)); + return $"{title} {headingText}".ToLowerInvariant(); + } + + private static bool ContainsAny(string text, params string[] keywords) + => keywords.Any(k => text.Contains(k, StringComparison.OrdinalIgnoreCase)); + private static int EstimateWordCount(string text) { if (string.IsNullOrWhiteSpace(text)) return 0; diff --git a/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs b/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs index c3d3c9c..c33c5b1 100644 --- a/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs +++ b/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs @@ -23,6 +23,18 @@ public class DocumentPlannerTool : IAgentTool return app?.SettingsService?.Settings.Llm.FolderDataUsage ?? "none"; } + private static string GetDefaultOutputFormat() + { + var app = System.Windows.Application.Current as App; + return app?.SettingsService?.Settings.Llm.DefaultOutputFormat ?? "auto"; + } + + private static string GetDefaultMood() + { + var app = System.Windows.Application.Current as App; + return app?.SettingsService?.Settings.Llm.DefaultMood ?? "modern"; + } + public string Name => "document_plan"; public string Description => "Create a structured document outline/plan and optionally generate the document file immediately. " + @@ -53,8 +65,8 @@ public class DocumentPlannerTool : IAgentTool ["format"] = new() { Type = "string", - Description = "Output document format: html, docx, markdown. Default: html", - Enum = ["html", "docx", "markdown"] + Description = "Output document format: auto, html, docx, markdown. Default: auto (resolved from settings and request intent)", + Enum = ["auto", "html", "docx", "markdown"] }, ["sections_hint"] = new() { @@ -75,7 +87,7 @@ public class DocumentPlannerTool : IAgentTool var topic = args.GetProperty("topic").GetString() ?? ""; var docType = args.TryGetProperty("document_type", out var dt) ? dt.GetString() ?? "report" : "report"; var targetPages = args.TryGetProperty("target_pages", out var tp) ? tp.GetInt32() : 5; - var format = args.TryGetProperty("format", out var fmt) ? fmt.GetString() ?? "html" : "html"; + var requestedFormat = args.TryGetProperty("format", out var fmt) ? fmt.GetString() ?? "auto" : GetDefaultOutputFormat(); var sectionsHint = args.TryGetProperty("sections_hint", out var sh) ? sh.GetString() ?? "" : ""; var refSummary = args.TryGetProperty("reference_summary", out var rs) ? rs.GetString() ?? "" : ""; @@ -87,6 +99,8 @@ public class DocumentPlannerTool : IAgentTool var highQuality = IsMultiPassEnabled(); var folderDataUsage = GetFolderDataUsage(); + var format = ResolveDocumentFormat(requestedFormat, docType, topic); + var mood = ResolveDocumentMood(GetDefaultMood(), docType, topic); // 고품질 모드: 목표 페이지와 단어 수를 1.5배 확장 var effectivePages = highQuality ? (int)Math.Ceiling(targetPages * 1.5) : targetPages; @@ -97,16 +111,16 @@ public class DocumentPlannerTool : IAgentTool // 폴더 데이터 활용 모드: 먼저 파일을 읽어야 하므로 별도 처리 if (folderDataUsage is "active" or "passive") - return ExecuteSinglePassWithData(topic, docType, format, effectivePages, totalWords, sections, folderDataUsage, refSummary); + return ExecuteSinglePassWithData(topic, docType, format, mood, effectivePages, totalWords, sections, folderDataUsage, refSummary); // 일반/고품질 모드 모두 동일 구조: // 개요 반환 + document_assemble 즉시 호출 지시 - return ExecuteWithAssembleInstructions(topic, docType, format, effectivePages, totalWords, sections, highQuality); + return ExecuteWithAssembleInstructions(topic, docType, format, mood, effectivePages, totalWords, sections, highQuality); } // ─── 통합: 포맷별 body 골격 생성 + 즉시 호출 가능한 도구 파라미터 제시 ────── - private Task ExecuteWithAssembleInstructions(string topic, string docType, string format, + private Task ExecuteWithAssembleInstructions(string topic, string docType, string format, string mood, int targetPages, int totalWords, List sections, bool highQuality) { var safeTitle = SanitizeFileName(topic); @@ -121,13 +135,13 @@ public class DocumentPlannerTool : IAgentTool case "docx": return ExecuteWithDocxScaffold(topic, suggestedFileName, sections, targetPages, totalWords, label); default: // html - return ExecuteWithHtmlScaffold(topic, suggestedFileName, sections, targetPages, totalWords, label); + return ExecuteWithHtmlScaffold(topic, suggestedFileName, sections, targetPages, totalWords, label, mood); } } /// html_create 즉시 호출 가능한 body 골격 반환. 섹션 구조만 고정하고 내부 시각화는 LLM이 자유롭게 선택. private Task ExecuteWithHtmlScaffold(string topic, string fileName, - List sections, int targetPages, int totalWords, string label) + List sections, int targetPages, int totalWords, string label, string mood) { var bodySb = new StringBuilder(); foreach (var s in sections) @@ -148,10 +162,10 @@ public class DocumentPlannerTool : IAgentTool output.AppendLine("## 즉시 실행: html_create 호출"); output.AppendLine($"path: \"{fileName}\""); output.AppendLine($"title: \"{topic}\""); - output.AppendLine("toc: true, numbered: true, mood: \"professional\""); + output.AppendLine($"toc: true, numbered: true, mood: \"{mood}\""); output.AppendLine($"cover: {{\"title\": \"{topic}\", \"author\": \"AX Copilot Agent\"}}"); output.AppendLine(); - output.AppendLine("body에 아래 섹션 구조를 기반으로 각 섹션의 내용과 시각화를 자유롭게 작성하세요:"); + output.AppendLine($"body에 아래 섹션 구조를 기반으로 각 섹션의 내용과 시각화를 자유롭게 작성하세요. (추천 디자인: {mood})"); output.AppendLine("(주석의 '활용 가능 요소'는 참고용이며, 내용에 맞게 다른 요소를 써도 됩니다)"); output.AppendLine(); output.AppendLine("--- body 시작 ---"); @@ -275,7 +289,7 @@ public class DocumentPlannerTool : IAgentTool // ─── 싱글패스 + 폴더 데이터 활용: 개요 반환 + LLM이 데이터 읽고 직접 저장 ── - private Task ExecuteSinglePassWithData(string topic, string docType, string format, + private Task ExecuteSinglePassWithData(string topic, string docType, string format, string mood, int targetPages, int totalWords, List sections, string folderDataUsage, string refSummary) { var ext = format switch { "docx" => ".docx", "markdown" => ".md", _ => ".html" }; @@ -307,11 +321,12 @@ public class DocumentPlannerTool : IAgentTool ? "(skipped)" : "Summarize the key findings from the folder documents relevant to the topic.", step3 = $"Write the COMPLETE document content covering ALL sections above, incorporating folder data.", - step4 = $"Save the document using {createTool} with the path '{suggestedPath}'. " + - "Write ALL sections in a SINGLE tool call. Do NOT split into multiple calls.", - note = "Minimize LLM calls. Read data → write complete document → save. Maximum 3 iterations." - } - }; + step4 = $"Save the document using {createTool} with the path '{suggestedPath}'. " + + "Write ALL sections in a SINGLE tool call. Do NOT split into multiple calls.", + preferred_mood = format == "html" ? mood : null as string, + note = "Minimize LLM calls. Read data → write complete document → save. Maximum 3 iterations." + } + }; var json = JsonSerializer.Serialize(plan, _jsonOptions); @@ -322,6 +337,7 @@ public class DocumentPlannerTool : IAgentTool return Task.FromResult(ToolResult.Ok( $"📋 문서 개요가 생성되었습니다 [싱글패스+데이터활용] ({sections.Count}개 섹션, 목표 {targetPages}페이지/{totalWords}단어).\n" + $"{dataNote}\n" + + $"{(format == "html" ? $"권장 디자인 무드: {mood}\n" : string.Empty)}" + $"작성 완료 후 {createTool}로 '{suggestedPath}' 파일에 저장하세요.\n\n{json}")); } @@ -584,6 +600,57 @@ public class DocumentPlannerTool : IAgentTool private static string Escape(string text) => text.Replace("&", "&").Replace("<", "<").Replace(">", ">"); + private static string ResolveDocumentFormat(string? preferred, string docType, string topic) + { + var normalized = (preferred ?? "auto").Trim().ToLowerInvariant(); + if (normalized is "html" or "docx" or "markdown") + return normalized; + + var intent = $"{docType} {topic}".ToLowerInvariant(); + + if (ContainsAny(intent, "docx", "word", "워드")) + return "docx"; + if (ContainsAny(intent, "markdown", ".md", "md ", "마크다운", "readme", "가이드", "manual", "guide", "회의록", "minutes")) + return "markdown"; + if (ContainsAny(intent, "html", "웹 문서", "웹페이지")) + return "html"; + + return docType switch + { + "manual" or "guide" or "minutes" => "markdown", + "proposal" or "presentation" => "docx", + "analysis" => "html", + _ => "docx", + }; + } + + private static string ResolveDocumentMood(string? preferred, string docType, string topic) + { + var normalized = (preferred ?? "modern").Trim().ToLowerInvariant(); + if (!string.IsNullOrWhiteSpace(normalized) && normalized != "modern") + return normalized; + + var intent = $"{docType} {topic}".ToLowerInvariant(); + if (ContainsAny(intent, "대시보드", "dashboard", "분석", "analysis", "지표", "통계")) + return "dashboard"; + if (ContainsAny(intent, "기획", "아이디어", "creative", "브레인스토밍")) + return "creative"; + if (ContainsAny(intent, "기업", "공식", "proposal", "제안서", "사내 보고")) + return "corporate"; + if (ContainsAny(intent, "가이드", "manual", "guide", "매뉴얼")) + return "minimal"; + + return docType switch + { + "proposal" => "corporate", + "analysis" => "dashboard", + "manual" or "guide" => "minimal", + "presentation" => "creative", + "minutes" => "professional", + _ => "professional", + }; + } + private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true,