Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions JobFlow.API/Controllers/AiWriterController.cs
Original file line number Diff line number Diff line change
@@ -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<IResult> 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);
15 changes: 15 additions & 0 deletions JobFlow.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
});
});

// ============================================================
Expand Down
58 changes: 58 additions & 0 deletions JobFlow.Business/Services/AiWriterService.cs
Original file line number Diff line number Diff line change
@@ -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<OpenAiSettings> openAiOptions)
{
_openAiSettings = openAiOptions.Value;
}

public async Task<Result<string>> DraftEstimateNotesAsync(Guid organizationId, string[] lineItemNames)
{
if (string.IsNullOrWhiteSpace(_openAiSettings.ApiKey))
return Result.Failure<string>(Error.Failure("AiWriter.NotConfigured", "AI writer is not configured."));

if (lineItemNames.Length == 0)
return Result.Failure<string>(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<ChatMessage>
{
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using JobFlow.Business;

namespace JobFlow.Business.Services.ServiceInterfaces;

public interface IAiWriterService
{
Task<Result<string>> DraftEstimateNotesAsync(Guid organizationId, string[] lineItemNames);
}
2 changes: 1 addition & 1 deletion JobFlow.Business/Services/SetupCompanionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading