Skip to content

Commit 04759ed

Browse files
Tonychen0227Tony Chen (DevDiv)
andauthored
Chentony/mermaid response refactor (Azure#21)
* Fix diagram prompting * Small change * Reminder * Fix * Remove Mermaid Encode/Decode --------- Co-authored-by: Tony Chen (DevDiv) <[email protected]>
1 parent 2066752 commit 04759ed

File tree

4 files changed

+57
-104
lines changed

4 files changed

+57
-104
lines changed

areas/deploy/src/AzureMcp.Deploy/Commands/Architecture/DiagramGenerateCommand.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,6 @@ public override Task<CommandResponse> ExecuteAsync(CommandContext context, Parse
8181
{
8282
throw new InvalidOperationException("Failed to generate architecture diagram. The chart content is empty.");
8383
}
84-
var encodedDiagram = EncodeMermaid.GetEncodedMermaidChart(chart).Replace("+", "-").Replace("/", "_"); // replace '+' with '-' and "/" with "_" for URL safety and consistency with mermaid.live URL encoding
85-
86-
var mermaidUrl = $"https://mermaid.live/view#pako:{encodedDiagram}";
87-
_logger.LogInformation("Generated architecture diagram successfully. Mermaid URL: {MermaidUrl}", mermaidUrl);
8884

8985
var usedServiceTypes = appTopology.Services
9086
.SelectMany(service => service.Dependencies)
@@ -99,8 +95,8 @@ public override Task<CommandResponse> ExecuteAsync(CommandContext context, Parse
9995
? string.Join(", ", usedServiceTypes)
10096
: null;
10197

102-
context.Response.Message = $"Help the user open up this URI to preview their app topology using tool open_simple_browser: {mermaidUrl} \n"
103-
+ "Ask user if the topology is expected, if not, you should call this tool with the user's updated instructions. "
98+
context.Response.Message = $"Here is the user's mermaid diagram. Please write this into .azure/architecture.copilot.md. Make changes if these do not fulfill requirements:\n ```mermaid\n{chart}\n``` \n"
99+
+ "Ask user if the topology is expected, if not, you should directly update the generated diagram with the user's updated instructions. Remind the user to install a Mermaid preview extension to be able to render the diagram. "
104100
+ "Please inform the user that here are the supported hosting technologies: "
105101
+ $"{string.Join(", ", Enum.GetNames<AzureServiceConstants.AzureComputeServiceType>())}. ";
106102
if (!string.IsNullOrWhiteSpace(usedServiceTypesString))

areas/deploy/src/AzureMcp.Deploy/Commands/GenerateMermaidChart.cs

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections.Immutable;
55
using System.Text;
6+
using Azure.ResourceManager.Network.Models;
67
using AzureMcp.Deploy.Options;
78
using Microsoft.Extensions.ObjectPool;
89

@@ -14,6 +15,9 @@ public static class GenerateMermaidChart
1415
private const string aksClusterInternalName = "akscluster";
1516
private const string aksClusterName = "Azure Kubernetes Service (AKS) Cluster";
1617

18+
private const string acaEnvInternalName = "acaenvironment";
19+
private const string acaEnvName = "Azure Container Apps Environment";
20+
1721
public static string GenerateChart(string workspaceFolder, AppTopology appTopology)
1822
{
1923
var chartComponents = new List<string>();
@@ -32,7 +36,8 @@ public static string GenerateChart(string workspaceFolder, AppTopology appTopolo
3236
}
3337

3438
var services = new List<string> { "%% Services" };
35-
var resources = new List<string> { "%% Resources" };
39+
var resources = new List<string> { "%% Compute Resources" };
40+
var dependencyResources = new List<string> { "%% Binding Resources" };
3641
var relationships = new List<string> { "%% Relationships" };
3742

3843
foreach (var service in appTopology.Services)
@@ -56,12 +61,14 @@ public static string GenerateChart(string workspaceFolder, AppTopology appTopolo
5661

5762
var serviceInternalName = $"svc-{service.Name}";
5863

59-
services.Add(CreateComponentName(serviceInternalName, string.Join("\n", serviceName), "service", NodeShape.Rectangle));
64+
services.Add(CreateComponentName(serviceInternalName, string.Join("\n", serviceName), NodeShape.Rectangle));
6065

6166
relationships.Add(CreateRelationshipString(serviceInternalName, $"{FlattenServiceType(service.AzureComputeHost)}_{service.Name}", "hosted on", ArrowType.Solid));
6267
}
6368

6469
var aksClusterExists = false;
70+
var containerAppEnvExists = false;
71+
6572
foreach (var service in appTopology.Services)
6673
{
6774
string serviceResourceInternalName = $"{FlattenServiceType(service.AzureComputeHost)}_{service.Name}";
@@ -74,18 +81,34 @@ public static string GenerateChart(string workspaceFolder, AppTopology appTopolo
7481
// containerized services share the same AKS cluster
7582
foreach (var aksservice in appTopology.Services.Where(s => s.AzureComputeHost == "aks"))
7683
{
77-
resources.Add(CreateComponentName($"{aksservice.AzureComputeHost}_{aksservice.Name}", $"{aksservice.Name} (Containerized Service)", "compute", NodeShape.RoundedRectangle));
84+
resources.Add(CreateComponentName($"{aksservice.AzureComputeHost}_{aksservice.Name}", $"{aksservice.Name} (Containerized Service)", NodeShape.RoundedRectangle));
7885
}
7986
resources.Add("end");
8087
resources.Add($"{aksClusterInternalName}:::cluster");
8188
aksClusterExists = true;
8289
}
8390
}
91+
else if (service.AzureComputeHost == "containerapp")
92+
{
93+
if (!containerAppEnvExists)
94+
{
95+
// Add Container App Environment as a subgraph
96+
resources.Add($"subgraph {acaEnvInternalName} [\"{acaEnvName}\"]");
97+
// containerized services share the same Container App Environment
98+
foreach (var containerAppService in appTopology.Services.Where(s => s.AzureComputeHost == "containerapp"))
99+
{
100+
resources.Add(CreateComponentName($"{containerAppService.AzureComputeHost}_{containerAppService.Name}", $"{containerAppService.Name} (Container App)", NodeShape.RoundedRectangle));
101+
}
102+
resources.Add("end");
103+
containerAppEnvExists = true;
104+
}
105+
}
84106
// each service should have a compute resource type
85107
else if (!resources.Any(r => r.Contains(serviceResourceInternalName)))
86108
{
87-
resources.Add(CreateComponentName(serviceResourceInternalName, $"{service.Name} ({GetFormalName(service.AzureComputeHost)})", "compute", NodeShape.RoundedRectangle));
109+
resources.Add(CreateComponentName(serviceResourceInternalName, $"{service.Name} ({GetFormalName(service.AzureComputeHost)})", NodeShape.RoundedRectangle));
88110
}
111+
89112
foreach (var dependency in service.Dependencies)
90113
{
91114
var instanceInternalName = $"{FlattenServiceType(dependency.ServiceType)}.{dependency.Name}";
@@ -95,12 +118,12 @@ public static string GenerateChart(string workspaceFolder, AppTopology appTopolo
95118
{
96119
if (!resources.Any(r => r.Contains(EnsureUrlFriendlyName(instanceInternalName))))
97120
{
98-
resources.Add(CreateComponentName(instanceInternalName, instanceName, "compute", NodeShape.RoundedRectangle));
121+
resources.Add(CreateComponentName(instanceInternalName, instanceName, NodeShape.RoundedRectangle));
99122
}
100123
}
101124
else
102125
{
103-
resources.Add(CreateComponentName(instanceInternalName, instanceName, "binding", NodeShape.Circle));
126+
dependencyResources.Add(CreateComponentName(instanceInternalName, instanceName, NodeShape.Rectangle));
104127
}
105128

106129
relationships.Add(CreateRelationshipString(serviceResourceInternalName, instanceInternalName, dependency.ConnectionType, ArrowType.Dotted));
@@ -109,15 +132,25 @@ public static string GenerateChart(string workspaceFolder, AppTopology appTopolo
109132

110133
chartComponents.AddRange(services);
111134
chartComponents.AddRange(resources);
135+
136+
137+
chartComponents.Add("subgraph \"Compute Resources\"");
138+
chartComponents.AddRange(resources);
139+
chartComponents.Add("end");
140+
141+
chartComponents.Add("subgraph \"Dependency Resources\"");
142+
chartComponents.AddRange(dependencyResources);
143+
chartComponents.Add("end");
144+
112145
chartComponents.AddRange(relationships);
113146

114147
return string.Join("\n", chartComponents);
115148
}
116149

117-
private static string CreateComponentName(string internalName, string name, string type, NodeShape nodeShape)
150+
private static string CreateComponentName(string internalName, string name, NodeShape nodeShape)
118151
{
119152
var nodeShapeBrackets = GetNodeShapeBrackets(nodeShape);
120-
return $"{EnsureUrlFriendlyName(internalName)}{nodeShapeBrackets[0]}\"`{name}`\"{nodeShapeBrackets[1]}:::{type}";
153+
return $"{EnsureUrlFriendlyName(internalName)}{nodeShapeBrackets[0]}\"`{name}`\"{nodeShapeBrackets[1]}";
121154
}
122155

123156
private static string CreateRelationshipString(string sourceName, string targetName, string connectionDescription, ArrowType arrowType)
@@ -200,7 +233,6 @@ private static bool IsComputeResourceType(string serviceType)
200233
{ "azureservicebus", "Azure Service Bus" },
201234
{ "azurewebpubsub", "Azure Web PubSub"}
202235
};
203-
204236
}
205237

206238
public enum NodeShape

areas/deploy/src/AzureMcp.Deploy/Models/EncodeMermaid.cs

Lines changed: 0 additions & 74 deletions
This file was deleted.

areas/deploy/tests/AzureMcp.Deploy.UnitTests/Commands/Architecture/DiagramGenerateCommandTests.cs

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -134,22 +134,21 @@ public async Task GenerateArchitectureDiagram_ShouldReturnEncryptedDiagramUrl()
134134
Assert.NotNull(response);
135135
Assert.Equal(200, response.Status);
136136
// Extract the URL from the response message
137-
var urlPattern = "https://mermaid.live/view#pako:";
138-
var urlStartIndex = response.Message.IndexOf(urlPattern);
139-
Assert.True(urlStartIndex >= 0, "URL starting with 'https://mermaid.live/view#pako:' should be present in the response");
137+
var graphStartPattern = "```mermaid";
138+
var graphStartIndex = response.Message.IndexOf(graphStartPattern);
139+
Assert.True(graphStartIndex >= 0, "Graph data starting with '```mermaid' should be present in the response");
140140

141-
// Extract the full URL (assuming it ends at whitespace or end of string)
142-
var urlStartPosition = urlStartIndex;
143-
var urlEndPosition = response.Message.IndexOfAny([' ', '\n', '\r', '\t'], urlStartPosition);
144-
if (urlEndPosition == -1)
145-
urlEndPosition = response.Message.Length;
141+
// Extract the full graph (assuming it ends at whitespace or end of string)
142+
var graphStartPosition = graphStartIndex;
143+
var graphEndPosition = response.Message.IndexOf("```", graphStartIndex + 1);
146144

147-
var extractedUrl = response.Message.Substring(urlStartPosition, urlEndPosition - urlStartPosition);
148-
Assert.StartsWith(urlPattern, extractedUrl);
149-
var encodedDiagram = extractedUrl.Substring(urlPattern.Length).Replace("_", "/").Replace("-", "+"); // Replace back for decoding
150-
var decodedDiagram = EncodeMermaid.GetDecodedMermaidChart(encodedDiagram);
151-
Assert.NotEmpty(decodedDiagram);
152-
Assert.Contains("website", decodedDiagram);
153-
Assert.Contains("store", decodedDiagram);
145+
if (graphEndPosition == -1)
146+
graphEndPosition = response.Message.Length;
147+
148+
var extractedGraph = response.Message.Substring(graphStartPosition, graphEndPosition - graphStartPosition);
149+
Assert.StartsWith(graphStartPattern, extractedGraph);
150+
Assert.NotEmpty(extractedGraph);
151+
Assert.Contains("website", extractedGraph);
152+
Assert.Contains("store", extractedGraph);
154153
}
155154
}

0 commit comments

Comments
 (0)