diff --git a/JobFlow.API/Controllers/AiWriterController.cs b/JobFlow.API/Controllers/AiWriterController.cs new file mode 100644 index 0000000..42f4e4a --- /dev/null +++ b/JobFlow.API/Controllers/AiWriterController.cs @@ -0,0 +1,35 @@ +using JobFlow.API.Extensions; +using JobFlow.Business.Extensions; +using JobFlow.Business.Services.ServiceInterfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; + +namespace JobFlow.API.Controllers; + +[ApiController] +[Route("api/ai")] +[Authorize] +public class AiWriterController : ControllerBase +{ + private readonly IAiWriterService _aiWriterService; + + public AiWriterController(IAiWriterService aiWriterService) + { + _aiWriterService = aiWriterService; + } + + [HttpPost("estimate-draft")] + [EnableRateLimiting("ai-writer")] + public async Task DraftEstimateNotes([FromBody] DraftEstimateNotesRequest request) + { + var organizationId = HttpContext.GetOrganizationId(); + var result = await _aiWriterService.DraftEstimateNotesAsync(organizationId, request.LineItemNames); + + return result.IsSuccess + ? Results.Ok(new { notes = result.Value }) + : result.ToProblemDetails(); + } +} + +public record DraftEstimateNotesRequest(string[] LineItemNames); diff --git a/JobFlow.API/Program.cs b/JobFlow.API/Program.cs index 852d0f6..30836ef 100644 --- a/JobFlow.API/Program.cs +++ b/JobFlow.API/Program.cs @@ -474,6 +474,21 @@ static JsonDocument ParseFirebaseAdminSdkJson(string json) AutoReplenishment = true }); }); + + // AI Writer (estimate draft) — 10 requests per hour per org + options.AddPolicy("ai-writer", context => + { + var orgId = context.User?.FindFirst("org_id")?.Value + ?? context.Connection.RemoteIpAddress?.ToString() + ?? "anonymous"; + return RateLimitPartition.GetFixedWindowLimiter($"ai-writer:{orgId}", _ => new FixedWindowRateLimiterOptions + { + PermitLimit = 10, + Window = TimeSpan.FromHours(1), + QueueLimit = 0, + AutoReplenishment = true + }); + }); }); // ============================================================ diff --git a/JobFlow.Business/Services/AiWriterService.cs b/JobFlow.Business/Services/AiWriterService.cs new file mode 100644 index 0000000..4e66998 --- /dev/null +++ b/JobFlow.Business/Services/AiWriterService.cs @@ -0,0 +1,58 @@ +using JobFlow.Business.ConfigurationSettings; +using JobFlow.Business.DI; +using JobFlow.Business.Services.ServiceInterfaces; +using Microsoft.Extensions.Options; +using OpenAI.Chat; + +namespace JobFlow.Business.Services; + +[ScopedService] +public class AiWriterService : IAiWriterService +{ + private readonly OpenAiSettings _openAiSettings; + + public AiWriterService(IOptions openAiOptions) + { + _openAiSettings = openAiOptions.Value; + } + + public async Task> DraftEstimateNotesAsync(Guid organizationId, string[] lineItemNames) + { + if (string.IsNullOrWhiteSpace(_openAiSettings.ApiKey)) + return Result.Failure(Error.Failure("AiWriter.NotConfigured", "AI writer is not configured.")); + + if (lineItemNames.Length == 0) + return Result.Failure(Error.Failure("AiWriter.NoLineItems", "At least one line item is required.")); + + var itemList = string.Join(", ", lineItemNames.Take(20)); + + var prompt = $""" + You are a professional field service contractor writing an estimate. Given the following services/line items: + + {itemList} + + Write a short, professional estimate notes section (2-4 sentences) that: + - Summarizes the scope of work in plain language + - Sets expectations for how the work will be performed + - Sounds professional and trustworthy + + Output only the notes text — no headers, no labels, no extra commentary. + """; + + var client = new ChatClient(_openAiSettings.Model, _openAiSettings.ApiKey); + + var messages = new List + { + new UserChatMessage(prompt) + }; + + var response = await client.CompleteChatAsync(messages, new ChatCompletionOptions + { + MaxOutputTokenCount = 200, + Temperature = 0.5f + }); + + var notes = response.Value.Content[0].Text.Trim(); + return Result.Success(notes); + } +} diff --git a/JobFlow.Business/Services/ServiceInterfaces/IAiWriterService.cs b/JobFlow.Business/Services/ServiceInterfaces/IAiWriterService.cs new file mode 100644 index 0000000..b9cfc0e --- /dev/null +++ b/JobFlow.Business/Services/ServiceInterfaces/IAiWriterService.cs @@ -0,0 +1,8 @@ +using JobFlow.Business; + +namespace JobFlow.Business.Services.ServiceInterfaces; + +public interface IAiWriterService +{ + Task> DraftEstimateNotesAsync(Guid organizationId, string[] lineItemNames); +} diff --git a/JobFlow.Business/Services/SetupCompanionService.cs b/JobFlow.Business/Services/SetupCompanionService.cs index 317b4ad..ca5e868 100644 --- a/JobFlow.Business/Services/SetupCompanionService.cs +++ b/JobFlow.Business/Services/SetupCompanionService.cs @@ -93,7 +93,7 @@ private static string BuildSystemPrompt(Organization org, string currentRoute) var plan = string.IsNullOrWhiteSpace(org.SubscriptionPlanName) ? "Go" : org.SubscriptionPlanName; return $""" - You are the JobFlow Setup Companion — a friendly, concise assistant embedded inside the JobFlow app. + You are Flow, the JobFlow Setup Companion — a friendly, concise assistant embedded inside the JobFlow app. JobFlow is a field service management platform for small businesses (contractors, cleaning companies, landscapers, IT services, HVAC, and similar trades). Your sole purpose is to help users understand and set up JobFlow. You answer ONLY questions about: