Skip to content

Commit c2ce935

Browse files
author
ricwilson
committed
Refactor OpenApiSpecGeneratorPlugin and PowerPlatformOpenApiSpecGeneratorPlugin for improved operation ID and description generation; streamline prompt files for parameter and response handling; enhance API title generation logic; remove unused prompt files.
1 parent 264d676 commit c2ce935

15 files changed

+485
-284
lines changed

DevProxy.Plugins/Generation/OpenApiSpecGeneratorPlugin.cs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e)
120120

121121
var openApiDocs = new List<OpenApiDocument>();
122122

123-
124123
foreach (var request in e.RequestLogs)
125124
{
126125
if (request.MessageType != MessageType.InterceptedResponse ||
@@ -145,17 +144,6 @@ request.Context.Session is null ||
145144
{
146145
var pathItem = GetOpenApiPathItem(request.Context.Session);
147146
var parametrizedPath = ParametrizePath(pathItem, request.Context.Session.HttpClient.Request.RequestUri);
148-
var operationInfo = pathItem.Operations.First();
149-
operationInfo.Value.OperationId = await GetOperationIdAsync(
150-
operationInfo.Key.ToString(),
151-
request.Context.Session.HttpClient.Request.RequestUri.GetLeftPart(UriPartial.Authority),
152-
parametrizedPath
153-
);
154-
operationInfo.Value.Description = await GetOperationDescriptionAsync(
155-
operationInfo.Key.ToString(),
156-
request.Context.Session.HttpClient.Request.RequestUri.GetLeftPart(UriPartial.Authority),
157-
parametrizedPath
158-
);
159147
await ProcessPathItemAsync(pathItem, request.Context.Session.HttpClient.Request.RequestUri, parametrizedPath);
160148
AddOrMergePathItem(openApiDocs, pathItem, request.Context.Session.HttpClient.Request.RequestUri, parametrizedPath);
161149
}
@@ -219,10 +207,22 @@ request.Context.Session is null ||
219207
/// <param name="requestUri">The request URI.</param>
220208
/// <param name="parametrizedPath">The parametrized path string.</param>
221209
/// <returns>The processed OpenApiPathItem.</returns>
222-
protected virtual Task ProcessPathItemAsync(OpenApiPathItem pathItem, Uri requestUri, string parametrizedPath)
210+
protected virtual async Task ProcessPathItemAsync(OpenApiPathItem pathItem, Uri requestUri, string parametrizedPath)
223211
{
224-
// By default, return the path item unchanged. Derived plugins can override to add/modify path-level data.
225-
return Task.CompletedTask;
212+
ArgumentNullException.ThrowIfNull(pathItem);
213+
ArgumentNullException.ThrowIfNull(requestUri);
214+
215+
var operationInfo = pathItem.Operations.First();
216+
operationInfo.Value.OperationId = await GetOperationIdAsync(
217+
operationInfo.Key.ToString(),
218+
requestUri.GetLeftPart(UriPartial.Authority),
219+
parametrizedPath
220+
);
221+
operationInfo.Value.Description = await GetOperationDescriptionAsync(
222+
operationInfo.Key.ToString(),
223+
requestUri.GetLeftPart(UriPartial.Authority),
224+
parametrizedPath
225+
);
226226
}
227227

228228
/// <summary>
@@ -248,7 +248,7 @@ protected virtual async Task<string> GetOperationIdAsync(string method, string s
248248
{ "request", $"{method.ToUpperInvariant()} {serverUrl}{parametrizedPath}" }
249249
});
250250
}
251-
return id?.Response ?? $"{method}{parametrizedPath.Replace('/', '.')}";
251+
return id?.Response?.Trim() ?? $"{method}{parametrizedPath.Replace('/', '.')}";
252252
}
253253

254254
protected virtual async Task<string> GetOperationDescriptionAsync(string method, string serverUrl, string parametrizedPath, string promptyFile = "api_operation_description")
@@ -263,7 +263,7 @@ protected virtual async Task<string> GetOperationDescriptionAsync(string method,
263263
{ "request", $"{method.ToUpperInvariant()} {serverUrl}{parametrizedPath}" }
264264
});
265265
}
266-
return description?.Response ?? $"{method} {parametrizedPath}";
266+
return description?.Response?.Trim() ?? $"{method} {parametrizedPath}";
267267
}
268268

269269
/**

DevProxy.Plugins/Generation/PowerPlatformOpenApiSpecGeneratorPlugin.cs

Lines changed: 51 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,15 @@ IConfigurationSection pluginConfigurationSection
3636
/// <param name="requestUri">The request URI for context.</param>
3737
/// <param name="parametrizedPath">The parametrized path for the operation.</param>
3838
/// <returns>The processed OpenAPI path item.</returns>
39-
protected override async Task ProcessPathItemAsync(OpenApiPathItem pathItem, Uri requestUri, string parametrizedPath)
39+
/// <exception cref="ArgumentNullException">Thrown if <paramref name="pathItem"/> or <paramref name="requestUri"/> is null.</exception>
40+
protected override async Task<Task> ProcessPathItemAsync(OpenApiPathItem pathItem, Uri requestUri, string parametrizedPath)
4041
{
4142
ArgumentNullException.ThrowIfNull(pathItem);
4243
ArgumentNullException.ThrowIfNull(requestUri);
4344

4445
await ProcessPathItemDetailsAsync(pathItem, requestUri, parametrizedPath);
46+
47+
return Task.CompletedTask;
4548
}
4649

4750
/// <summary>
@@ -54,18 +57,16 @@ protected override async Task ProcessOpenApiDocumentAsync(OpenApiDocument openAp
5457
{
5558
ArgumentNullException.ThrowIfNull(openApiDoc);
5659
openApiDoc.Info.Contact = Configuration.Contact?.ToOpenApiContact();
57-
SetTitleAndDescription(openApiDoc);
5860

5961
// Try to get the server URL from the OpenAPI document
6062
var serverUrl = openApiDoc.Servers?.FirstOrDefault()?.Url;
61-
6263
if (string.IsNullOrWhiteSpace(serverUrl))
6364
{
6465
throw new InvalidOperationException("No server URL found in the OpenAPI document. Please ensure the document contains at least one server definition.");
6566
}
6667

67-
// Asynchronously call the metadata generator
68-
var metadata = await GenerateConnectorMetadataAsync(serverUrl);
68+
var (apiDescription, operationDescriptions) = await SetTitleAndDescription(openApiDoc, serverUrl);
69+
var metadata = await GenerateConnectorMetadataAsync(serverUrl, apiDescription, operationDescriptions);
6970
openApiDoc.Extensions["x-ms-connector-metadata"] = metadata;
7071
RemoveConnectorMetadataExtension(openApiDoc);
7172
}
@@ -74,32 +75,48 @@ protected override async Task ProcessOpenApiDocumentAsync(OpenApiDocument openAp
7475
/// Sets the OpenApi title and description in the Info area of the OpenApiDocument using LLM-generated values.
7576
/// </summary>
7677
/// <param name="openApiDoc">The OpenAPI document to process.</param>
77-
private void SetTitleAndDescription(OpenApiDocument openApiDoc)
78+
/// <param name="serverUrl">The server URL to use as a fallback for title and description.</param>
79+
/// <returns>A tuple containing the API description and a list of operation descriptions.</returns>
80+
private async Task<(string apiDescription, string operationDescriptions)> SetTitleAndDescription(OpenApiDocument openApiDoc, string serverUrl)
7881
{
79-
// Synchronously call the async title/description generators
80-
var defaultTitle = openApiDoc.Info?.Title ?? "API";
81-
var defaultDescription = openApiDoc.Info?.Description ?? "API description.";
82-
var title = GetOpenApiTitleAsync(defaultTitle).GetAwaiter().GetResult();
83-
var description = GetOpenApiDescriptionAsync(defaultDescription).GetAwaiter().GetResult();
82+
var defaultTitle = openApiDoc.Info?.Title ?? serverUrl;
83+
var defaultDescription = openApiDoc.Info?.Description ?? serverUrl;
84+
var operationDescriptions = string.Join(
85+
Environment.NewLine,
86+
openApiDoc.Paths?
87+
.SelectMany(p => p.Value.Operations.Values)
88+
.Select(op => op.Description)
89+
.Where(desc => !string.IsNullOrWhiteSpace(desc))
90+
.Distinct()
91+
.Select(d => $"- {d}") ?? []
92+
);
93+
94+
var title = await GetOpenApiTitleAsync(defaultTitle, operationDescriptions);
95+
var description = await GetOpenApiDescriptionAsync(defaultDescription, operationDescriptions);
8496
openApiDoc.Info ??= new OpenApiInfo();
8597
openApiDoc.Info.Title = title;
8698
openApiDoc.Info.Description = description;
99+
100+
return (description, operationDescriptions);
87101
}
88102

89103
/// <summary>
90104
/// Removes the x-ms-connector-metadata extension from the OpenAPI document if it exists
91105
/// and is empty.
92106
/// </summary>
93107
/// <param name="openApiDoc">The OpenAPI document to process.</param>
94-
private async Task<string> GetOpenApiDescriptionAsync(string defaultDescription)
108+
/// <param name="operationDescriptions">Operation descriptions to use for generating the OpenAPI description.</param>
109+
/// <returns>The OpenAPI description generated by LLM or the default description.</returns>
110+
private async Task<string> GetOpenApiDescriptionAsync(string defaultDescription, string operationDescriptions)
95111
{
96112
ILanguageModelCompletionResponse? description = null;
97113

98114
if (await _languageModelClient.IsEnabledAsync())
99115
{
100116
description = await _languageModelClient.GenerateChatCompletionAsync("powerplatform_api_description", new()
101117
{
102-
{ "description", defaultDescription }
118+
{ "defaultDescription", defaultDescription },
119+
{ "operationDescriptions", operationDescriptions }
103120
});
104121
}
105122

@@ -110,14 +127,17 @@ private async Task<string> GetOpenApiDescriptionAsync(string defaultDescription)
110127
/// Generates a concise and descriptive title for the OpenAPI document using LLM or fallback logic.
111128
/// </summary>
112129
/// <param name="defaultTitle">The default title to use if LLM generation fails.</param>
113-
private async Task<string> GetOpenApiTitleAsync(string defaultTitle)
130+
/// <param name="operationDescriptions">A list of operation descriptions for context.</param>
131+
/// <returns>The generated title.</returns>
132+
private async Task<string> GetOpenApiTitleAsync(string defaultTitle, string operationDescriptions)
114133
{
115134
ILanguageModelCompletionResponse? title = null;
116135

117136
if (await _languageModelClient.IsEnabledAsync())
118137
{
119138
title = await _languageModelClient.GenerateChatCompletionAsync("powerplatform_api_title", new() {
120-
{ "defaultTitle", defaultTitle }
139+
{ "defaultTitle", defaultTitle },
140+
{ "operationDescriptions", operationDescriptions }
121141
});
122142
}
123143

@@ -448,11 +468,13 @@ private static string GetResponsePropertyDescriptionFallback(string propertyName
448468
/// Generates the connector metadata OpenAPI extension array using configuration and LLM.
449469
/// </summary>
450470
/// <param name="serverUrl">The server URL for context.</param>
471+
/// <param name="apiDescription">The API description for context.</param>
472+
/// <param name="operationDescriptions">A list of operation descriptions for context.</param>
451473
/// <returns>An <see cref="OpenApiArray"/> containing connector metadata.</returns>
452-
private async Task<OpenApiArray> GenerateConnectorMetadataAsync(string serverUrl)
474+
private async Task<OpenApiArray> GenerateConnectorMetadataAsync(string serverUrl, string apiDescription, string operationDescriptions)
453475
{
454-
var website = Configuration.ConnectorMetadata?.Website ?? await GetConnectorMetadataWebsiteUrlAsync(serverUrl);
455-
var privacyPolicy = Configuration.ConnectorMetadata?.PrivacyPolicy ?? await GetConnectorMetadataPrivacyPolicyUrlAsync(serverUrl);
476+
var website = Configuration.ConnectorMetadata?.Website ?? serverUrl;
477+
var privacyPolicy = Configuration.ConnectorMetadata?.PrivacyPolicy ?? serverUrl;
456478

457479
string categories;
458480
var categoriesList = Configuration.ConnectorMetadata?.Categories;
@@ -462,7 +484,7 @@ private async Task<OpenApiArray> GenerateConnectorMetadataAsync(string serverUrl
462484
}
463485
else
464486
{
465-
categories = await GetConnectorMetadataCategoriesAsync(serverUrl, "Data");
487+
categories = await GetConnectorMetadataCategoriesAsync(serverUrl, apiDescription, operationDescriptions);
466488
}
467489

468490
var metadataArray = new OpenApiArray
@@ -486,70 +508,32 @@ private async Task<OpenApiArray> GenerateConnectorMetadataAsync(string serverUrl
486508
return metadataArray;
487509
}
488510

489-
/// <summary>
490-
/// Generates the website URL for connector metadata using LLM or configuration.
491-
/// </summary>
492-
/// <param name="defaultUrl">The default URL to use if LLM fails.</param>
493-
/// <returns>The website URL.</returns>
494-
private async Task<string> GetConnectorMetadataWebsiteUrlAsync(string defaultUrl)
495-
{
496-
ILanguageModelCompletionResponse? response = null;
497-
498-
if (await _languageModelClient.IsEnabledAsync())
499-
{
500-
response = await _languageModelClient.GenerateChatCompletionAsync("powerplatform_api_connector_metadata_website", new()
501-
{
502-
{ "defaultUrl", defaultUrl }
503-
});
504-
}
505-
506-
// Fallback to the default URL if the language model fails or returns no response
507-
return !string.IsNullOrWhiteSpace(response?.Response) ? response.Response.Trim() : defaultUrl;
508-
}
509-
510-
/// <summary>
511-
/// Generates the privacy policy URL for connector metadata using LLM or configuration.
512-
/// </summary>
513-
/// <param name="defaultUrl">The default URL to use if LLM fails.</param>
514-
/// <returns>The privacy policy URL.</returns>
515-
private async Task<string> GetConnectorMetadataPrivacyPolicyUrlAsync(string defaultUrl)
516-
{
517-
ILanguageModelCompletionResponse? response = null;
518-
519-
if (await _languageModelClient.IsEnabledAsync())
520-
{
521-
response = await _languageModelClient.GenerateChatCompletionAsync("powerplatform_api_connector_metadata_privacy_policy", new()
522-
{
523-
{ "defaultUrl", defaultUrl }
524-
});
525-
}
526-
527-
// Fallback to the default URL if the language model fails or returns no response
528-
return !string.IsNullOrWhiteSpace(response?.Response) ? response.Response.Trim() : defaultUrl;
529-
}
530-
531511
/// <summary>
532512
/// Generates the categories for connector metadata using LLM or configuration.
533513
/// </summary>
534514
/// <param name="serverUrl">The server URL for context.</param>
535-
/// <param name="defaultCategories">The default categories to use if LLM fails.</param>
536-
/// <returns>The categories string.</returns>
537-
private async Task<string> GetConnectorMetadataCategoriesAsync(string serverUrl, string defaultCategories)
515+
/// <param name="apiDescription">The API description for context.</param>
516+
/// <param name="operationDescriptions">A list of operation descriptions for context.</param>
517+
/// <returns>A string containing the categories for the connector metadata.</returns>
518+
/// <exception cref="InvalidOperationException">Thrown if the language model is not enabled and
519+
private async Task<string> GetConnectorMetadataCategoriesAsync(string serverUrl, string apiDescription, string operationDescriptions)
538520
{
539521
ILanguageModelCompletionResponse? response = null;
540522

541523
if (await _languageModelClient.IsEnabledAsync())
542524
{
543525
response = await _languageModelClient.GenerateChatCompletionAsync("powerplatform_api_connector_metadata_categories", new()
544526
{
545-
{ "serverUrl", serverUrl }
527+
{ "serverUrl", serverUrl },
528+
{ "apiDescription", apiDescription },
529+
{ "operationDescriptions", operationDescriptions }
546530
});
547531
}
548532

549533
// If the response is 'None' or empty, return the default categories
550534
return !string.IsNullOrWhiteSpace(response?.Response) && response.Response.Trim() != "None"
551-
? response.Response
552-
: defaultCategories;
535+
? response.Response.Trim()
536+
: "Data";
553537
}
554538

555539
/// <summary>

DevProxy/DevProxy.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
<None Update="prompts\powerplatform_api_operation_id.prompty">
8383
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
8484
</None>
85+
<None Update="prompts\powerplatform_api_operation_summary.prompty">
86+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
87+
</None>
8588
<None Update="prompts\powerplatform_api_parameter_description.prompty">
8689
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
8790
</None>

DevProxy/prompts/powerplatform_api_connector_metadata_categories.prompty

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
---
2-
name: OpenAPI allowed categories
3-
description: Determine the most appropriate categories for an API from the Microsoft Power Platform allowed list.
2+
name: Power Platform OpenAPI Categories
3+
description: Classify the API into one or more Microsoft Power Platform allowed categories based on the API metadata and server URL.
44
authors:
55
- Dev Proxy
66
model:
77
api: chat
88
sample:
99
request: |
1010
Server URL: https://api.example.com
11-
Response: Data
11+
API Description: A service that provides document collaboration features
12+
Operation Descriptions:
13+
- Share a document with another user.
14+
- Retrieve a list of collaborators.
15+
- Update document permissions.
16+
Response: Collaboration, Content and Files
17+
response: |
18+
Collaboration, Content and Files
1219
---
1320

1421
system:
15-
You're an expert in OpenAPI and API documentation. Based on the following API metadata and the server URL, determine the most appropriate categories for the API from the allowed list of categories. If you cannot determine appropriate categories, respond with 'None'.
22+
You are an expert in OpenAPI and Microsoft Power Platform custom connectors. Your task is to classify an API based on its metadata and purpose using only the categories allowed by Power Platform.
1623

17-
API Metadata:
18-
- Server URL: {{serverUrl}}
24+
These categories are used in the Power Platform custom connector metadata field `x-ms-connector-metadata.categories`.
1925

2026
Allowed Categories:
2127
- AI
@@ -39,15 +45,24 @@ Allowed Categories:
3945
- Website
4046

4147
Rules you must follow:
42-
- Do not output any explanations or additional text.
43-
- The categories must be from the allowed list.
44-
- The categories must be relevant to the API's functionality and purpose.
45-
- The categories should be in a comma-separated format.
46-
- If you cannot determine appropriate categories, respond with 'None'.
48+
- Only return categories from the allowed list.
49+
- Choose categories relevant to the API's core functionality.
50+
- Return a comma-separated list of categories, no more than 3.
51+
- If no appropriate category can be confidently determined, return `None`.
52+
- Do not include explanations or additional text.
4753

4854
Example:
49-
Server URL: https://api.example.com
50-
Response: Data
55+
Server URL: https://api.example.com
56+
API Description: A service that provides document collaboration features
57+
Operation Descriptions:
58+
- Share a document with another user.
59+
- Retrieve a list of collaborators.
60+
- Update document permissions.
61+
Response: Collaboration, Content and Files
5162

5263
user:
53-
Server URL: {{serverUrl}}
64+
Server URL: {{serverUrl}}
65+
API Description: {{apiDescription}}
66+
Operation Descriptions:
67+
{{operationDescriptions}}
68+
Response:

0 commit comments

Comments
 (0)