Forge
csharpdeeb25a2
1#nullable enable
2
3using System.Text;
4
5namespace CascadeIDE.Services;
6
7/// <summary>
8/// Тексты MAF IDE-агента: <see cref="BundledRelativePath"/> — диск под exe затем EmbeddedResource (<see cref="BundledAppContent"/>).
9/// Секции в файле: строки <c>## agent_system</c>, <c>## salvage_recap_system</c>, <c>## salvage_recap_user_message</c>.
10/// </summary>
11internal static class MafIdeAgentPrompts
12{
13 /// <summary>Относительно <see cref="AppContext.BaseDirectory"/>.</summary>
14 internal const string BundledRelativePath = "AiPrompts/maf-ide-agent.prompts.md";
15
16 private const string AgentSystemSection = "agent_system";
17 private const string SalvageSystemSection = "salvage_recap_system";
18 private const string SalvageUserSection = "salvage_recap_user_message";
19
20 private static readonly Lazy<PromptPack> Loaded = new(ParsePromptFile, LazyThreadSafetyMode.ExecutionAndPublication);
21
22 internal sealed record PromptPack(
23 string AgentSystem,
24 string SalvageRecapSystem,
25 string SalvageUserMessageTemplate,
26 IReadOnlyDictionary<string, string> OptionalSections)
27 {
28 internal string BuildSalvageUserMessage(string userQuery, string toolPayload) =>
29 SalvageUserMessageTemplate
30 .Replace("{{USER_QUERY}}", userQuery ?? "", StringComparison.Ordinal)
31 .Replace("{{TOOL_PAYLOAD}}", toolPayload ?? "", StringComparison.Ordinal);
32
33 internal bool TryGetOptionalSection(string key, out string text)
34 {
35 if (OptionalSections.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value))
36 {
37 text = value;
38 return true;
39 }
40
41 text = "";
42 return false;
43 }
44 }
45
46 internal static PromptPack Current => Loaded.Value;
47
48 private static PromptPack ParsePromptFile()
49 {
50 if (!BundledAppContent.TryReadDiskThenEmbedded(BundledRelativePath, out var raw) || string.IsNullOrWhiteSpace(raw))
51 throw new InvalidOperationException(
52 $"Отсутствует или пустой {BundledRelativePath} (рядом с exe или EmbeddedResource сборки CascadeIDE).");
53
54 var sections = SplitSections(raw);
55 if (!sections.TryGetValue(AgentSystemSection, out var agent) || string.IsNullOrWhiteSpace(agent))
56 throw new InvalidOperationException($"{BundledRelativePath}: нужна секция ## {AgentSystemSection}.");
57 if (!sections.TryGetValue(SalvageSystemSection, out var salvageSys) || string.IsNullOrWhiteSpace(salvageSys))
58 throw new InvalidOperationException($"{BundledRelativePath}: нужна секция ## {SalvageSystemSection}.");
59 if (!sections.TryGetValue(SalvageUserSection, out var salvageUser) || string.IsNullOrWhiteSpace(salvageUser))
60 throw new InvalidOperationException($"{BundledRelativePath}: нужна секция ## {SalvageUserSection}.");
61 if (!salvageUser.Contains("{{USER_QUERY}}", StringComparison.Ordinal) ||
62 !salvageUser.Contains("{{TOOL_PAYLOAD}}", StringComparison.Ordinal))
63 throw new InvalidOperationException(
64 $"{BundledRelativePath}: в ## {SalvageUserSection} должны быть плейсхолдеры {{USER_QUERY}} и {{TOOL_PAYLOAD}}.");
65
66 return new PromptPack(
67 agent.Trim(),
68 salvageSys.Trim(),
69 salvageUser.Trim(),
70 BuildOptionalSections(sections));
71 }
72
73 private static IReadOnlyDictionary<string, string> BuildOptionalSections(IReadOnlyDictionary<string, string> sections)
74 {
75 var optional = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
76 foreach (var pair in sections)
77 {
78 if (string.Equals(pair.Key, AgentSystemSection, StringComparison.OrdinalIgnoreCase) ||
79 string.Equals(pair.Key, SalvageSystemSection, StringComparison.OrdinalIgnoreCase) ||
80 string.Equals(pair.Key, SalvageUserSection, StringComparison.OrdinalIgnoreCase))
81 {
82 continue;
83 }
84
85 var text = pair.Value?.Trim();
86 if (!string.IsNullOrWhiteSpace(text))
87 optional[pair.Key] = text;
88 }
89
90 return optional;
91 }
92
93 private static Dictionary<string, string> SplitSections(string raw)
94 {
95 var sections = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
96 using var reader = new StringReader(raw);
97 string? currentKey = null;
98 var sb = new StringBuilder();
99 string? line;
100 while ((line = reader.ReadLine()) != null)
101 {
102 if (line.StartsWith("## ", StringComparison.Ordinal) && line.Length > 3)
103 {
104 if (currentKey is not null)
105 sections[currentKey] = sb.ToString().TrimEnd();
106 sb.Clear();
107 var keyPart = line[3..].Trim();
108 var firstToken = keyPart.Split([' ', '\t'], 2, StringSplitOptions.RemoveEmptyEntries);
109 currentKey = firstToken.Length > 0 ? firstToken[0].Trim().Trim('\uFEFF') : null;
110 if (string.IsNullOrEmpty(currentKey))
111 currentKey = null;
112 continue;
113 }
114
115 if (currentKey is not null)
116 {
117 sb.AppendLine(line);
118 }
119 }
120
121 if (currentKey is not null)
122 sections[currentKey] = sb.ToString().TrimEnd();
123
124 return sections;
125 }
126}
127
View only · write via MCP/CIDE