Skip to content

Commit f425c29

Browse files
authored
Use AI to recommend task steps (#21)
* WIP on AI suggestions for Steps * Create client service for suggestions * Need to change model * Refactoring * Suggests task definition steps * Improve modal dialog behaviors with closing * Remove Optional from form, needs future work
1 parent 4be5e38 commit f425c29

File tree

12 files changed

+318
-8
lines changed

12 files changed

+318
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
@using MaintenanceLog.Common.Contracts
2+
@using MaintenanceLog.Data.Entities
3+
4+
@inject ISmartTaskDefinitionStepService SmartTaskDefinitionStepService
5+
6+
@if (isLoading)
7+
{
8+
<div class="spinner-border text-primary" role="status">
9+
<span class="visually-hidden">Loading...</span>
10+
</div>
11+
}
12+
else
13+
{
14+
<button type="button" class="btn btn-primary" @onclick="@OnSmartAssist">
15+
Recommend Steps
16+
</button>
17+
}
18+
19+
@if (aiResultSteps is not null)
20+
{
21+
<div class="modal fade show" style="display: block;" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
22+
<div class="modal-dialog modal-lg">
23+
<div class="modal-content">
24+
<div class="modal-header">
25+
<h5 class="modal-title" id="exampleModalLabel">Smart Assist</h5>
26+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="@Cancel"></button>
27+
</div>
28+
<div class="modal-body p-0">
29+
<span>
30+
Add the following steps to your task:
31+
</span>
32+
<table class="table table-striped">
33+
<thead>
34+
<tr>
35+
<th scope="col">Name</th>
36+
<th scope="col">Is Optional</th>
37+
<th scope="col">Is Selected</th>
38+
</tr>
39+
</thead>
40+
<tbody>
41+
@foreach (var step in aiResultSteps)
42+
{
43+
<tr>
44+
<td>@step.Name</td>
45+
<td>
46+
<div class="form-check form-switch">
47+
@* <InputCheckbox class="form-check-input" Value="@step.IsOptional" /> shouldn't be nullable *@
48+
</div>
49+
</td>
50+
<td>
51+
<div class="form-check form-switch">
52+
<InputCheckbox class="form-check-input" @bind-Value="@step.IsSelected" />
53+
</div>
54+
</td>
55+
</tr>
56+
}
57+
</tbody>
58+
</table>
59+
</div>
60+
<div class="modal-footer">
61+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="@Cancel">Close</button>
62+
<button type="button" class="btn btn-primary" @onclick="@SaveSteps" disabled="@(aiResultSteps.Any(s => s.IsSelected) == false)">Add steps</button>
63+
</div>
64+
</div>
65+
</div>
66+
</div>
67+
}
68+
69+
@code
70+
{
71+
[Parameter]
72+
public TaskDefinition? TaskDefinition { get; set; }
73+
[Parameter]
74+
public EventCallback<TaskDefinition> DataChangedAsync { get; set; }
75+
76+
private List<SuggestedTaskDefinitionStep>? aiResultSteps;
77+
private bool isLoading = false;
78+
79+
protected override async Task OnParametersSetAsync()
80+
{
81+
await base.OnParametersSetAsync();
82+
if (TaskDefinition is null)
83+
{
84+
throw new ArgumentNullException(nameof(TaskDefinition));
85+
}
86+
}
87+
88+
protected override async Task OnInitializedAsync()
89+
{
90+
await base.OnInitializedAsync();
91+
}
92+
93+
private async Task OnSmartAssist()
94+
{
95+
isLoading = true;
96+
97+
string taskDescriptor = string.Empty;
98+
if (!string.IsNullOrWhiteSpace(TaskDefinition!.Area?.Name))
99+
{
100+
taskDescriptor += $"in {TaskDefinition.Area.Name}";
101+
}
102+
if (!string.IsNullOrWhiteSpace(TaskDefinition!.Asset?.Name))
103+
{
104+
taskDescriptor += $" for {TaskDefinition.Asset.Name}";
105+
}
106+
107+
var steps = await SmartTaskDefinitionStepService.SuggestStepsForTaskDefinition(TaskDefinition!.Name, taskDescriptor, TaskDefinition!.TaskDefinitionSteps?.Select(s => s.Name));
108+
109+
// show modal with steps that user can check to include (default check them all)
110+
// then save the steps to the task definition
111+
// if user cancels, do nothing
112+
// if user saves, update the task definition with the new steps
113+
// if user modifies the steps, update the task definition with the new steps
114+
115+
var suggestedSteps = steps?.Select(s => new SuggestedTaskDefinitionStep(s) { Name = s, IsSelected = false })?.ToList();
116+
117+
isLoading = false;
118+
119+
if (suggestedSteps is null)
120+
{
121+
// do something about this?
122+
return;
123+
}
124+
125+
// populating aiResultSteps will show the modal
126+
aiResultSteps = suggestedSteps;
127+
128+
return;
129+
}
130+
131+
private void Cancel()
132+
{
133+
aiResultSteps = null;
134+
}
135+
136+
private async Task SaveSteps()
137+
{
138+
if (aiResultSteps is null)
139+
{
140+
return;
141+
}
142+
143+
var selectedSteps = aiResultSteps.Where(s => s.IsSelected).ToList();
144+
TaskDefinition!.TaskDefinitionSteps ??= new List<TaskDefinitionStep>();
145+
146+
foreach (var step in selectedSteps)
147+
{
148+
TaskDefinition.TaskDefinitionSteps.Add(new TaskDefinitionStep
149+
{
150+
Name = step.Name,
151+
IsOptional = step.IsOptional
152+
});
153+
}
154+
155+
aiResultSteps = null;
156+
await DataChangedAsync.InvokeAsync(TaskDefinition);
157+
}
158+
159+
// AI response will populate into this object
160+
// and allow user to modify IsSelected to determine what is
161+
// added to the TaskDefinition
162+
class SuggestedTaskDefinitionStep : TaskDefinitionStep
163+
{
164+
public SuggestedTaskDefinitionStep(string taskDefinitionStepName)
165+
{
166+
this.Name = taskDefinitionStepName;
167+
this.IsSelected = true;
168+
}
169+
public bool IsSelected { get; set; } = false;
170+
}
171+
}

src/MaintenanceLog.Client/Extensions/ServiceCollectionExtensions.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ public static IServiceCollection AddMaintenanceLogClientServices(this IServiceCo
1818
services.AddBlazoredLocalStorage();
1919

2020
services.AddTransient(sp => new ModuleCreator(sp.GetService<IJSRuntime>()));
21-
services.AddScoped(http => new HttpClient { BaseAddress = new Uri(hostBuilder.HostEnvironment.BaseAddress) });
22-
21+
services.AddScoped(http => new HttpClient { BaseAddress = new Uri(hostBuilder.HostEnvironment.BaseAddress) });
22+
2323
services.AddScoped<ISmartScheduleService, HttpSmartScheduleService>();
24+
services.AddScoped<ISmartTaskDefinitionStepService, HttpSmartTaskDefinitionStepService>();
2425

2526
return services;
2627
}

src/MaintenanceLog.Client/Pages/TaskDefinitionSteps/TaskDefinitionStepGrid.razor

+5
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@
4242
protected override async Task OnInitializedAsync()
4343
{
4444
await base.OnInitializedAsync();
45+
}
46+
47+
protected override async Task OnParametersSetAsync()
48+
{
49+
await base.OnParametersSetAsync();
4550
await LoadTaskDefinitionSteps();
4651
}
4752

src/MaintenanceLog.Client/Pages/TaskDefinitions/EditTaskDefinition.razor

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@page "/task-definitions/edit/{TaskDefinitionId:int}"
22

33
@using MaintenanceLog.Client.Components.Schedules
4+
@using MaintenanceLog.Client.Components.TaskDefinitions
45
@using MaintenanceLog.Client.Pages.TaskDefinitionSteps
56
@using MaintenanceLog.Data.Entities
67
@using MaintenanceLog.Data.Services.Contracts
@@ -70,15 +71,15 @@
7071
<div class="form-group">
7172
<label for="cronSchedule">Cron Schedule</label>
7273
<div class="form-check form-switch">
73-
<InputCheckbox id="isAdvancedCron" class="form-check-input" @bind-Value="isAdvancedCron" /> Advanced Edit?
74+
<InputCheckbox id="isAdvancedCron" class="form-check-input" @bind-Value="@isAdvancedCron" /> Advanced Edit?
7475
</div>
7576

76-
<SmartCronScheduleEditor Item="taskDefinition" DataChangedAsync="async (TaskDefinition td) => await OnDataChanged(td)" />
77+
<SmartCronScheduleEditor Item="@taskDefinition" DataChangedAsync="async (TaskDefinition td) => await OnDataChanged(td)" />
7778

7879
<!-- if toggle is checked, show InputText -->
7980
@if (isAdvancedCron)
8081
{
81-
<InputText id="cronSchedule" class="form-control" @bind-Value="taskDefinition.CronSchedule" />
82+
<InputText id="cronSchedule" class="form-control" @bind-Value="@taskDefinition.CronSchedule" />
8283
}
8384

8485
<CronScheduleAlert CronSchedule="@taskDefinition.CronSchedule" />
@@ -90,7 +91,9 @@
9091
</div>
9192
</EditForm>
9293

93-
<TaskDefinitionStepGrid TaskDefinitionId="TaskDefinitionId" />
94+
<TaskDefinitionStepGrid TaskDefinitionId="@TaskDefinitionId" />
95+
<br />
96+
<SmartTaskDefinitionStepEditor TaskDefinition="@taskDefinition" DataChangedAsync="async (TaskDefinition td) => await OnDataChanged(td)" />
9497
}
9598
else
9699
{
@@ -141,6 +144,7 @@ else
141144
private async Task OnDataChanged(TaskDefinition taskDefinition)
142145
{
143146
this.taskDefinition = taskDefinition;
147+
StateHasChanged();
144148
await InvokeAsync(StateHasChanged);
145149
}
146150
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using MaintenanceLog.Common.Contracts;
2+
using MaintenanceLog.Common.Models.Requests;
3+
using MaintenanceLog.Common.Models.Responses;
4+
using MaintenanceLog.Data.Services.Client;
5+
6+
namespace MaintenanceLog.Client.Services
7+
{
8+
public class HttpSmartTaskDefinitionStepService(JsonHttpClient httpClient) : ISmartTaskDefinitionStepService
9+
{
10+
private readonly JsonHttpClient _httpClient = httpClient;
11+
12+
public async Task<List<string>?> SuggestStepsForTaskDefinition(string? name, string? description = null, IEnumerable<string>? taskDefinitionSteps = null, IEnumerable<string>? overrideSystemPrompts = null)
13+
{
14+
var response = await _httpClient.PostAsJsonAsync<SuggestTaskDefinitionStepsRequest, SuggestTaskDefinitionStepsResponse>("api/task-definition-steps/suggest",
15+
new SuggestTaskDefinitionStepsRequest
16+
{
17+
Name = name,
18+
Description = description,
19+
OverrideSystemPrompts = overrideSystemPrompts?.ToArray(),
20+
Steps = taskDefinitionSteps?.ToArray()
21+
});
22+
return response is null
23+
? throw new Exception("API did not return suggested steps")
24+
: response.Steps?.ToList();
25+
}
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace MaintenanceLog.Common.Contracts;
2+
public interface ISmartTaskDefinitionStepService
3+
{
4+
public Task<List<string>?> SuggestStepsForTaskDefinition(string? name, string? description = null, IEnumerable<string>? taskDefinitionSteps = null, IEnumerable<string>? overrideSystemPrompts = null);
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace MaintenanceLog.Common.Models.Requests;
2+
3+
public record SuggestTaskDefinitionStepsRequest
4+
{
5+
public string? Name { get; set; }
6+
public string? Description { get; set; }
7+
public string[]? Steps { get; set; }
8+
public string[]? OverrideSystemPrompts { get; set; }
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace MaintenanceLog.Common.Models.Responses;
2+
3+
public record SuggestTaskDefinitionStepsResponse
4+
{
5+
public IEnumerable<string>? Steps { get; set; }
6+
}

src/MaintenanceLog.Data/Services/Server/TaskDefinitionStepService.cs

+8
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ public async Task DeleteAsync(int id)
2626
var result = await context.TaskDefinitionSteps!.FindAsync(id);
2727
if (result != null)
2828
{
29+
var associatedTaskInstanceSteps = await context.TaskInstanceSteps!
30+
.Where(tis => tis.TaskDefinitionStepId == id)
31+
.ToListAsync();
32+
foreach (var taskInstanceStep in associatedTaskInstanceSteps)
33+
{
34+
context.TaskInstanceSteps!.Remove(taskInstanceStep);
35+
}
36+
await context.SaveChangesAsync();
2937
context.TaskDefinitionSteps.Remove(result);
3038
await context.SaveChangesAsync();
3139
}

src/MaintenanceLog/Controllers/TaskDefinitionStepController.cs

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
using MaintenanceLog.Data.Entities;
1+
using MaintenanceLog.Common.Contracts;
2+
using MaintenanceLog.Common.Models.Requests;
3+
using MaintenanceLog.Common.Models.Responses;
4+
using MaintenanceLog.Data.Entities;
25
using MaintenanceLog.Data.Services.Contracts;
36
using Microsoft.AspNetCore.Authorization;
47
using Microsoft.AspNetCore.Mvc;
@@ -14,16 +17,19 @@ public class TaskDefinitionStepController: ControllerBase
1417
private readonly ITaskDefinitionService _taskDefinitionService;
1518
private readonly ITaskInstanceService _taskInstanceService;
1619
private readonly ITaskInstanceStepService _taskInstanceStepService;
20+
private readonly ISmartTaskDefinitionStepService _smartTaskDefinitionStepService;
1721
public TaskDefinitionStepController(
1822
ITaskDefinitionStepService taskDefinitionStepService,
1923
ITaskDefinitionService taskDefinitionService,
2024
ITaskInstanceService taskInstanceService,
21-
ITaskInstanceStepService taskInstanceStepService)
25+
ITaskInstanceStepService taskInstanceStepService,
26+
ISmartTaskDefinitionStepService smartTaskDefinitionStepService)
2227
{
2328
_taskDefinitionStepService = taskDefinitionStepService;
2429
_taskDefinitionService = taskDefinitionService;
2530
_taskInstanceService = taskInstanceService;
2631
_taskInstanceStepService = taskInstanceStepService;
32+
_smartTaskDefinitionStepService = smartTaskDefinitionStepService;
2733
}
2834

2935
[HttpGet]
@@ -80,5 +86,17 @@ public async Task<ActionResult<TaskInstanceStep>> CreateTaskInstanceStep(int tas
8086
};
8187
return Ok(await _taskInstanceStepService.AddAsync(taskInstanceStep));
8288
}
89+
90+
[HttpPost]
91+
[Route("suggest")]
92+
public async Task<ActionResult<List<string>>> EstimateTaskDefinitionSteps([FromBody] SuggestTaskDefinitionStepsRequest request)
93+
{
94+
var suggestedSteps = await _smartTaskDefinitionStepService.SuggestStepsForTaskDefinition(request.Name, request.Description, request.Steps, request.OverrideSystemPrompts);
95+
if (suggestedSteps is null)
96+
{
97+
return BadRequest("Unable to suggest any steps.");
98+
}
99+
return Ok(new SuggestTaskDefinitionStepsResponse { Steps = suggestedSteps });
100+
}
83101
}
84102
}

src/MaintenanceLog/Extensions/ServiceCollectionExtensions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public static IServiceCollection AddMaintenanceLogServices(this IServiceCollecti
2525

2626
services.AddScoped<IOpenAIService, OpenAIService>();
2727
services.AddScoped<ISmartScheduleService, OpenAISmartScheduleService>();
28+
services.AddScoped<ISmartTaskDefinitionStepService, OpenAISmartTaskDefinitionStepService>();
2829

2930
return services;
3031
}

0 commit comments

Comments
 (0)