코워크 문서 생성 기본 포맷 편향 완화
Some checks failed
Release Gate / gate (push) Has been cancelled

문서 생성 도구가 인자 없이 호출될 때 html/professional로 고정되던 기본값을 제거했습니다.

DocumentPlannerTool과 DocumentAssemblerTool이 설정값(DefaultOutputFormat, DefaultMood), 문서 유형, 요청 키워드를 함께 보고 docx/html/markdown 및 corporate/dashboard/minimal/creative/professional 무드를 자동 선택하도록 조정했습니다.

README와 DEVELOPMENT 문서에 변경 배경과 검증 결과를 반영했고, dotnet build 기준 경고 0 오류 0을 확인했습니다.
This commit is contained in:
2026-04-06 15:55:12 +09:00
parent 2ae56b2510
commit 8da0a069b7
4 changed files with 159 additions and 21 deletions

View File

@@ -10,6 +10,17 @@ namespace AxCopilot.Services.Agent;
/// </summary>
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;