문서 생성 도구가 인자 없이 호출될 때 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:
@@ -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;
|
||||
|
||||
@@ -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<ToolResult> ExecuteWithAssembleInstructions(string topic, string docType, string format,
|
||||
private Task<ToolResult> ExecuteWithAssembleInstructions(string topic, string docType, string format, string mood,
|
||||
int targetPages, int totalWords, List<SectionPlan> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>html_create 즉시 호출 가능한 body 골격 반환. 섹션 구조만 고정하고 내부 시각화는 LLM이 자유롭게 선택.</summary>
|
||||
private Task<ToolResult> ExecuteWithHtmlScaffold(string topic, string fileName,
|
||||
List<SectionPlan> sections, int targetPages, int totalWords, string label)
|
||||
List<SectionPlan> 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<ToolResult> ExecuteSinglePassWithData(string topic, string docType, string format,
|
||||
private Task<ToolResult> ExecuteSinglePassWithData(string topic, string docType, string format, string mood,
|
||||
int targetPages, int totalWords, List<SectionPlan> 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,
|
||||
|
||||
Reference in New Issue
Block a user