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
65 changes: 62 additions & 3 deletions src/Domain/Entities/OpenAI/OpenAiPrompt.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Domain.Entities.OpenAI;

public class OpenAiPrompt : HasDatesBase
public class OpenAiPrompt : HasDatesBase, IHasIdBase<OpenAiPromptType>
{
public const string DefaultCompanyAnalyzePrompt =
"You are a helpful career assistant. " +
Expand All @@ -16,6 +17,23 @@ public class OpenAiPrompt : HasDatesBase
"You are a helpful assistant. Analyze the user's input and provide a response. " +
"Your reply should be in question language, markdown formatted.";

private static readonly List<string> _chatGptAllowedModels = new List<string>
{
"gpt-3.5-turbo",
"gpt-4",
"gpt-4o",
};

private static readonly List<string> _claudeAllowedModels = new List<string>
{
"claude-3-5-haiku-20241022",
"claude-3-5-haiku-latest",
"claude-3-5-sonnet-20241022",
"claude-3-5-sonnet-latest",
"claude-sonnet-4-20250514",
"claude-sonnet-4-0",
};

[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public OpenAiPromptType Id { get; protected set; }
Expand All @@ -33,9 +51,11 @@ public OpenAiPrompt(
AiEngine engine)
{
Id = id;
Prompt = prompt;
Model = model;
Prompt = prompt?.Trim();
Model = model?.Trim().ToLowerInvariant();
Engine = engine;

ValidateModel();
}

// for migrations
Expand All @@ -56,4 +76,43 @@ public OpenAiPrompt(
protected OpenAiPrompt()
{
}

public void Update(
string prompt,
string model,
AiEngine engine)
{
prompt = prompt?.Trim();
if (string.IsNullOrEmpty(prompt))
{
throw new InvalidOperationException("Prompt cannot be null or empty.");
}

model = model?.Trim().ToLowerInvariant();
if (string.IsNullOrEmpty(model))
{
throw new InvalidOperationException("Model cannot be null or empty.");
}

Prompt = prompt;
Model = model;
Engine = engine;

ValidateModel();
}

public void ValidateModel()
{
if (Engine is AiEngine.OpenAi &&
!_chatGptAllowedModels.Contains(Model))
{
throw new InvalidOperationException($"Model '{Model}' is not allowed for OpenAI engine.");
}

if (Engine is AiEngine.Claude &&
!_claudeAllowedModels.Contains(Model))
{
throw new InvalidOperationException($"Model '{Model}' is not allowed for Claude engine.");
}
}
}
8 changes: 8 additions & 0 deletions src/Infrastructure/Database/ContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,14 @@ public static async Task<T> ByIdOrFailAsync<T>(
?? throw NotFoundException.CreateFromEntity<T>(id);
}

public static Task<T> ByIdOrNullAsync<T, TKey>(
this IQueryable<T> query,
TKey id,
CancellationToken cancellationToken = default)
where T : class, IHasIdBase<TKey>
where TKey : struct =>
query.FirstOrDefaultAsync(x => x.Id.Equals(id), cancellationToken);

public static async Task<Pageable<TEntity>> AsPaginatedAsync<TEntity>(
this IQueryable<TEntity> query,
PageModel pageModelOrNull = null,
Expand Down
128 changes: 128 additions & 0 deletions src/Web.Api/Features/Admin/AiPrompts/AiPromptController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Domain.Entities.OpenAI;
using Domain.Enums;
using Domain.Validation.Exceptions;
using Infrastructure.Authentication.Contracts;
using Infrastructure.Database;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Web.Api.Features.Admin.AiPrompts.Models;
using Web.Api.Setup.Attributes;

namespace Web.Api.Features.Admin.AiPrompts;

[HasAnyRole(Role.Admin)]
[ApiController]
[Route("api/admin/ai-prompts")]
public class AiPromptController : ControllerBase
{
private readonly IAuthorization _auth;
private readonly DatabaseContext _context;

public AiPromptController(
IAuthorization auth,
DatabaseContext context)
{
_auth = auth;
_context = context;
}

[HttpGet("")]
public async Task<List<OpenAiPromptDto>> All(
CancellationToken cancellationToken)
{
await _auth.HasRoleOrFailAsync(Role.Admin, cancellationToken);

return await _context.OpenAiPrompts
.OrderBy(x => x.Id)
.Select(x => new OpenAiPromptDto
{
Id = x.Id,
Prompt = x.Prompt,
Model = x.Model,
Engine = x.Engine,
CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt
})
.AsNoTracking()
.ToListAsync(cancellationToken);
}

[HttpPost("")]
public async Task<IActionResult> Create(
[FromBody] OpenAiPromptEditRequest createRequest,
CancellationToken cancellationToken)
{
await _auth.HasRoleOrFailAsync(Role.Admin, cancellationToken);

if (!createRequest.Id.HasValue)
{
throw new BadRequestException("Id is required for prompt creation");
}

if (createRequest.Engine is AiEngine.Undefined)
{
throw new BadRequestException("Engine is required for prompt creation");
}

if (await _context.OpenAiPrompts.AnyAsync(
x => x.Id == createRequest.Id.Value,
cancellationToken: cancellationToken))
{
throw new BadRequestException("Prompt with this ID already exists");
}

var item = await _context.SaveAsync(
new OpenAiPrompt(
createRequest.Id.Value,
createRequest.Prompt,
createRequest.Model,
createRequest.Engine),
cancellationToken);

return Ok(item.Id);
}

[HttpPut("")]
public async Task<IActionResult> Update(
[FromBody] OpenAiPromptEditRequest updateRequest,
CancellationToken cancellationToken)
{
await _auth.HasRoleOrFailAsync(Role.Admin, cancellationToken);

if (!updateRequest.Id.HasValue)
{
throw new BadRequestException("Id is required for OpenAI prompt update");
}

var entity = await _context.OpenAiPrompts
.FirstOrDefaultAsync(x => x.Id == updateRequest.Id.Value, cancellationToken);

entity.Update(
updateRequest.Prompt,
updateRequest.Model,
updateRequest.Engine);

await _context.TrySaveChangesAsync(cancellationToken);
return Ok();
}

[HttpDelete("{id}")]
public async Task<IActionResult> Delete(
[FromRoute] OpenAiPromptType id,
CancellationToken cancellationToken)
{
await _auth.HasRoleOrFailAsync(Role.Admin, cancellationToken);

var entity = await _context.OpenAiPrompts
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);

_context.Remove(entity);
await _context.TrySaveChangesAsync(cancellationToken);

return Ok();
}
}
19 changes: 19 additions & 0 deletions src/Web.Api/Features/Admin/AiPrompts/Models/OpenAiPromptDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using Domain.Entities.OpenAI;

namespace Web.Api.Features.Admin.AiPrompts.Models;

public record OpenAiPromptDto
{
public OpenAiPromptType Id { get; init; }

public string Prompt { get; init; }

public string Model { get; init; }

public AiEngine Engine { get; init; }

public DateTimeOffset CreatedAt { get; init; }

public DateTimeOffset UpdatedAt { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
using Domain.Entities.OpenAI;

namespace Web.Api.Features.Admin.AiPrompts.Models;

public record OpenAiPromptEditRequest
{
public OpenAiPromptType? Id { get; init; }

[Required]
public string Prompt { get; init; }

[Required]
public string Model { get; init; }

[Required]
public AiEngine Engine { get; init; }
}